ztechno_cli 0.0.2
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 +1 -0
- package/README.md +159 -0
- package/lib/index.d.ts +0 -0
- package/lib/index.js +0 -0
- package/lib/scripts/docker-build.d.ts +14 -0
- package/lib/scripts/docker-build.js +89 -0
- package/lib/scripts/docker-publish.d.ts +20 -0
- package/lib/scripts/docker-publish.js +115 -0
- package/lib/scripts/docker-push.d.ts +12 -0
- package/lib/scripts/docker-push.js +85 -0
- package/lib/scripts/docker-update.d.ts +29 -0
- package/lib/scripts/docker-update.js +209 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# ztechno_cli
|
|
2
|
+
|
|
3
|
+
CLI tools for the ZTechno framework to build, push, and remotely update Docker containers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ztechno_cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use via `npx`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ztechno_cli <command>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### `ztechno-cli-build`
|
|
22
|
+
|
|
23
|
+
Builds a Docker image tagged as `<awsAccountId>/<packagename>:<tag>`.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ztechno-cli-build [options]
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
--name <name> Image name (default: package.json "name")
|
|
30
|
+
--account <id> AWS Account ID (default: package.json "config.awsAccountId")
|
|
31
|
+
--tag <tag> Image tag (default: "latest")
|
|
32
|
+
--context <path> Build context path (default: ".")
|
|
33
|
+
-h, --help Show help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
### `ztechno-cli-push`
|
|
39
|
+
|
|
40
|
+
Pushes a Docker image to the registry.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ztechno-cli-push [options]
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--name <name> Image name (default: package.json "name")
|
|
47
|
+
--account <id> AWS Account ID (default: package.json "config.awsAccountId")
|
|
48
|
+
--tag <tag> Image tag (default: "latest")
|
|
49
|
+
-h, --help Show help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### `ztechno-cli-update`
|
|
55
|
+
|
|
56
|
+
Triggers a remote Docker container update via the ZTechno V2 Secure API (HMAC-signed).
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ztechno-cli-update <packagename>:<port>
|
|
60
|
+
ztechno-cli-update # auto-detect from .env / package.json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Auto-detection priority:**
|
|
64
|
+
1. `.env` file — `hostname`, `packagename`, `port`, `volumes`, `ZTECHNO_API_SECRET`
|
|
65
|
+
2. `package.json` — `"name"` as packagename, `"config.hostname"`, `"config.port"`, `"config.volumes"`
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### `ztechno-cli-publish`
|
|
70
|
+
|
|
71
|
+
Runs the full pipeline: **build → push → remote update**.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
ztechno-cli-publish [options]
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--name <name> Image name (default: package.json "name")
|
|
78
|
+
--account <id> AWS Account ID (default: package.json "config.awsAccountId")
|
|
79
|
+
--tag <tag> Image tag (default: "latest")
|
|
80
|
+
--context <path> Build context path (default: ".")
|
|
81
|
+
-h, --help Show help
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The remote update step is skipped automatically if `hostname` or `port` is not configured.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
### `.env` file
|
|
91
|
+
|
|
92
|
+
Place a `.env` file in your project root:
|
|
93
|
+
|
|
94
|
+
```env
|
|
95
|
+
ZTECHNO_API_SECRET=your_secret_here
|
|
96
|
+
hostname=192.168.1.100
|
|
97
|
+
packagename=my-image
|
|
98
|
+
port=3000
|
|
99
|
+
volumes=/host/data:/app/data,/host/logs:/app/logs
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `package.json`
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"name": "my-image",
|
|
107
|
+
"config": {
|
|
108
|
+
"awsAccountId": "000123456789",
|
|
109
|
+
"hostname": "192.168.1.100",
|
|
110
|
+
"port": 3000,
|
|
111
|
+
"volumes": [
|
|
112
|
+
"/host/data:/app/data",
|
|
113
|
+
"/host/logs:/app/logs"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Programmatic Usage
|
|
122
|
+
|
|
123
|
+
All commands are also exported as functions for use in your own scripts.
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { dockerBuild } from 'ztechno_cli/lib/scripts/docker-build'
|
|
127
|
+
import { dockerPush } from 'ztechno_cli/lib/scripts/docker-push'
|
|
128
|
+
import { updateDocker } from 'ztechno_cli/lib/scripts/docker-update'
|
|
129
|
+
import { dockerPublish } from 'ztechno_cli/lib/scripts/docker-publish'
|
|
130
|
+
|
|
131
|
+
// Build
|
|
132
|
+
dockerBuild({ packagename: 'my-image', awsAccountId: '000123456789' })
|
|
133
|
+
|
|
134
|
+
// Push
|
|
135
|
+
dockerPush({ packagename: 'my-image', awsAccountId: '000123456789' })
|
|
136
|
+
|
|
137
|
+
// Remote update
|
|
138
|
+
await updateDocker({
|
|
139
|
+
hostname: '192.168.1.100',
|
|
140
|
+
packagename: 'my-image',
|
|
141
|
+
port: 3000,
|
|
142
|
+
volumes: ['/host/data:/app/data'],
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Full pipeline
|
|
146
|
+
await dockerPublish()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Security
|
|
152
|
+
|
|
153
|
+
Remote update requests are authenticated with an **HMAC-SHA256** signature derived from `ZTECHNO_API_SECRET`. The signature covers the timestamp, package name, port, and volumes to prevent replay and tampering attacks.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT — Ivan Auda (ZTechnologies International)
|
package/lib/index.d.ts
ADDED
|
File without changes
|
package/lib/index.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Build a Docker image tagged as <awsAccountId>/<packagename>:latest
|
|
4
|
+
* @param opt.packagename - Override the image name (defaults to package.json name)
|
|
5
|
+
* @param opt.awsAccountId - Override the AWS Account ID (defaults to package.json config.awsAccountId)
|
|
6
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
7
|
+
* @param opt.context - Docker build context path (defaults to ".")
|
|
8
|
+
*/
|
|
9
|
+
export declare function dockerBuild(opt?: {
|
|
10
|
+
packagename?: string;
|
|
11
|
+
awsAccountId?: string;
|
|
12
|
+
tag?: string;
|
|
13
|
+
context?: string;
|
|
14
|
+
}): void;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.dockerBuild = dockerBuild;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
// ANSI color helpers
|
|
12
|
+
const red = (msg) => `\x1b[31m${msg}\x1b[0m`;
|
|
13
|
+
const green = (msg) => `\x1b[32m${msg}\x1b[0m`;
|
|
14
|
+
const cyan = (msg) => `\x1b[36m${msg}\x1b[0m`;
|
|
15
|
+
function loadPackageJson() {
|
|
16
|
+
const pkgPath = path_1.default.join(process.cwd(), 'package.json');
|
|
17
|
+
if (!fs_1.default.existsSync(pkgPath)) {
|
|
18
|
+
throw new Error(`No package.json found in ${process.cwd()}`);
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build a Docker image tagged as <awsAccountId>/<packagename>:latest
|
|
24
|
+
* @param opt.packagename - Override the image name (defaults to package.json name)
|
|
25
|
+
* @param opt.awsAccountId - Override the AWS Account ID (defaults to package.json config.awsAccountId)
|
|
26
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
27
|
+
* @param opt.context - Docker build context path (defaults to ".")
|
|
28
|
+
*/
|
|
29
|
+
function dockerBuild(opt) {
|
|
30
|
+
const pkg = loadPackageJson();
|
|
31
|
+
const packagename = opt?.packagename || pkg.name;
|
|
32
|
+
const awsAccountId = opt?.awsAccountId || pkg.config?.awsAccountId || process.env.AWS_ACCOUNT_ID;
|
|
33
|
+
const tag = opt?.tag || 'latest';
|
|
34
|
+
const context = opt?.context || '.';
|
|
35
|
+
if (!packagename) {
|
|
36
|
+
throw new Error('Missing package name. Set "name" in package.json or pass --name.');
|
|
37
|
+
}
|
|
38
|
+
if (!awsAccountId) {
|
|
39
|
+
throw new Error('Missing AWS Account ID. Set "config.awsAccountId" in package.json or AWS_ACCOUNT_ID env var.');
|
|
40
|
+
}
|
|
41
|
+
const image = `${awsAccountId}/${packagename}:${tag}`;
|
|
42
|
+
const cmd = `docker build -t ${image} ${context}`;
|
|
43
|
+
console.log(green(`[Docker Build]`));
|
|
44
|
+
console.log(cyan(`> ${cmd}`));
|
|
45
|
+
(0, child_process_1.execSync)(cmd, { stdio: 'inherit' });
|
|
46
|
+
console.log(green(`✓ Built ${image}`));
|
|
47
|
+
}
|
|
48
|
+
const USAGE = `
|
|
49
|
+
Usage:
|
|
50
|
+
ztechno-cli-build [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--name <name> Override image name (default: package.json "name")
|
|
54
|
+
--account <id> Override AWS Account ID (default: package.json "config.awsAccountId")
|
|
55
|
+
--tag <tag> Override tag (default: "latest")
|
|
56
|
+
--context <path> Docker build context (default: ".")
|
|
57
|
+
-h, --help Show this help
|
|
58
|
+
|
|
59
|
+
package.json format:
|
|
60
|
+
{
|
|
61
|
+
"name": "my-image",
|
|
62
|
+
"config": {
|
|
63
|
+
"awsAccountId": "00028463827"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`.trim();
|
|
67
|
+
if (require.main === module) {
|
|
68
|
+
try {
|
|
69
|
+
const args = process.argv.slice(2);
|
|
70
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
71
|
+
console.log(USAGE);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
const getArg = (flag) => {
|
|
75
|
+
const idx = args.indexOf(flag);
|
|
76
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
77
|
+
};
|
|
78
|
+
dockerBuild({
|
|
79
|
+
packagename: getArg('--name'),
|
|
80
|
+
awsAccountId: getArg('--account'),
|
|
81
|
+
tag: getArg('--tag'),
|
|
82
|
+
context: getArg('--context'),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error(red(`✗ Error: ${err.message}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run the full Docker publish pipeline: build → push → update
|
|
4
|
+
* All options are auto-detected from package.json and .env
|
|
5
|
+
* @param opt.packagename - Override the image name
|
|
6
|
+
* @param opt.awsAccountId - Override the AWS Account ID
|
|
7
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
8
|
+
* @param opt.context - Docker build context path (defaults to ".")
|
|
9
|
+
* @param opt.port - Override the port for remote update
|
|
10
|
+
* @param opt.volumes - Override volumes for remote update
|
|
11
|
+
*/
|
|
12
|
+
export declare function dockerPublish(opt?: {
|
|
13
|
+
packagename?: string;
|
|
14
|
+
awsAccountId?: string;
|
|
15
|
+
tag?: string;
|
|
16
|
+
context?: string;
|
|
17
|
+
hostname?: string;
|
|
18
|
+
port?: string | number;
|
|
19
|
+
volumes?: string[];
|
|
20
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.dockerPublish = dockerPublish;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const dotenv_1 = require("dotenv");
|
|
11
|
+
const docker_build_1 = require("./docker-build");
|
|
12
|
+
const docker_push_1 = require("./docker-push");
|
|
13
|
+
const docker_update_1 = require("./docker-update");
|
|
14
|
+
// ANSI color helpers
|
|
15
|
+
const red = (msg) => `\x1b[31m${msg}\x1b[0m`;
|
|
16
|
+
const green = (msg) => `\x1b[32m${msg}\x1b[0m`;
|
|
17
|
+
const cyan = (msg) => `\x1b[36m${msg}\x1b[0m`;
|
|
18
|
+
function loadPackageJson() {
|
|
19
|
+
const pkgPath = path_1.default.join(process.cwd(), 'package.json');
|
|
20
|
+
if (!fs_1.default.existsSync(pkgPath)) {
|
|
21
|
+
throw new Error(`No package.json found in ${process.cwd()}`);
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run the full Docker publish pipeline: build → push → update
|
|
27
|
+
* All options are auto-detected from package.json and .env
|
|
28
|
+
* @param opt.packagename - Override the image name
|
|
29
|
+
* @param opt.awsAccountId - Override the AWS Account ID
|
|
30
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
31
|
+
* @param opt.context - Docker build context path (defaults to ".")
|
|
32
|
+
* @param opt.port - Override the port for remote update
|
|
33
|
+
* @param opt.volumes - Override volumes for remote update
|
|
34
|
+
*/
|
|
35
|
+
async function dockerPublish(opt) {
|
|
36
|
+
// Load .env so ZTECHNO_API_SECRET (and other vars) are available
|
|
37
|
+
(0, dotenv_1.config)({ path: path_1.default.join(process.cwd(), '.env') });
|
|
38
|
+
const pkg = loadPackageJson();
|
|
39
|
+
const packagename = opt?.packagename || pkg.name;
|
|
40
|
+
const awsAccountId = opt?.awsAccountId || pkg.config?.awsAccountId || process.env.AWS_ACCOUNT_ID;
|
|
41
|
+
const tag = opt?.tag || 'latest';
|
|
42
|
+
console.log(cyan(`\n[Docker Publish Pipeline]`));
|
|
43
|
+
console.log(cyan(` Image: ${awsAccountId}/${packagename}:${tag}\n`));
|
|
44
|
+
// Step 1: Build
|
|
45
|
+
console.log(cyan(`── Step 1/3: Build ──`));
|
|
46
|
+
(0, docker_build_1.dockerBuild)({ packagename, awsAccountId, tag, context: opt?.context });
|
|
47
|
+
// Step 2: Push
|
|
48
|
+
console.log(cyan(`\n── Step 2/3: Push ──`));
|
|
49
|
+
(0, docker_push_1.dockerPush)({ packagename, awsAccountId, tag });
|
|
50
|
+
// Step 3: Update remote
|
|
51
|
+
console.log(cyan(`\n── Step 3/3: Update Remote ──`));
|
|
52
|
+
const hostname = opt?.hostname || pkg.config?.hostname || process.env.hostname;
|
|
53
|
+
const port = opt?.port || pkg.config?.port;
|
|
54
|
+
let volumes = opt?.volumes;
|
|
55
|
+
if (!volumes && pkg.config?.volumes) {
|
|
56
|
+
volumes = Array.isArray(pkg.config.volumes)
|
|
57
|
+
? pkg.config.volumes
|
|
58
|
+
: pkg.config.volumes.split(',').filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
if (!port || !hostname) {
|
|
61
|
+
console.log(green(`✓ Build & push complete. Skipping remote update (no port or hostname configured).`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await (0, docker_update_1.updateDocker)({
|
|
65
|
+
hostname,
|
|
66
|
+
packagename: packagename,
|
|
67
|
+
port,
|
|
68
|
+
volumes,
|
|
69
|
+
c: console,
|
|
70
|
+
});
|
|
71
|
+
console.log(green(`\n✓ Publish pipeline complete!`));
|
|
72
|
+
}
|
|
73
|
+
const USAGE = `
|
|
74
|
+
Usage:
|
|
75
|
+
ztechno-cli-publish [options]
|
|
76
|
+
|
|
77
|
+
Runs: docker build → docker push → remote update
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--name <name> Override image name (default: package.json "name")
|
|
81
|
+
--account <id> Override AWS Account ID (default: package.json "config.awsAccountId")
|
|
82
|
+
--tag <tag> Override tag (default: "latest")
|
|
83
|
+
--context <path> Docker build context (default: ".")
|
|
84
|
+
-h, --help Show this help
|
|
85
|
+
|
|
86
|
+
package.json format:
|
|
87
|
+
{
|
|
88
|
+
"name": "my-image",
|
|
89
|
+
"config": {
|
|
90
|
+
"awsAccountId": "00028463827",
|
|
91
|
+
"port": 3000,
|
|
92
|
+
"volumes": ["/data:/app/data"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
`.trim();
|
|
96
|
+
if (require.main === module) {
|
|
97
|
+
const args = process.argv.slice(2);
|
|
98
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
99
|
+
console.log(USAGE);
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
const getArg = (flag) => {
|
|
103
|
+
const idx = args.indexOf(flag);
|
|
104
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
105
|
+
};
|
|
106
|
+
dockerPublish({
|
|
107
|
+
packagename: getArg('--name'),
|
|
108
|
+
awsAccountId: getArg('--account'),
|
|
109
|
+
tag: getArg('--tag'),
|
|
110
|
+
context: getArg('--context'),
|
|
111
|
+
}).catch(err => {
|
|
112
|
+
console.error(red(`✗ Error: ${err.message}`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Push a Docker image tagged as <awsAccountId>/<packagename>:latest
|
|
4
|
+
* @param opt.packagename - Override the image name (defaults to package.json name)
|
|
5
|
+
* @param opt.awsAccountId - Override the AWS Account ID (defaults to package.json config.awsAccountId)
|
|
6
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
7
|
+
*/
|
|
8
|
+
export declare function dockerPush(opt?: {
|
|
9
|
+
packagename?: string;
|
|
10
|
+
awsAccountId?: string;
|
|
11
|
+
tag?: string;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.dockerPush = dockerPush;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
// ANSI color helpers
|
|
12
|
+
const red = (msg) => `\x1b[31m${msg}\x1b[0m`;
|
|
13
|
+
const green = (msg) => `\x1b[32m${msg}\x1b[0m`;
|
|
14
|
+
const cyan = (msg) => `\x1b[36m${msg}\x1b[0m`;
|
|
15
|
+
function loadPackageJson() {
|
|
16
|
+
const pkgPath = path_1.default.join(process.cwd(), 'package.json');
|
|
17
|
+
if (!fs_1.default.existsSync(pkgPath)) {
|
|
18
|
+
throw new Error(`No package.json found in ${process.cwd()}`);
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Push a Docker image tagged as <awsAccountId>/<packagename>:latest
|
|
24
|
+
* @param opt.packagename - Override the image name (defaults to package.json name)
|
|
25
|
+
* @param opt.awsAccountId - Override the AWS Account ID (defaults to package.json config.awsAccountId)
|
|
26
|
+
* @param opt.tag - Override the tag (defaults to "latest")
|
|
27
|
+
*/
|
|
28
|
+
function dockerPush(opt) {
|
|
29
|
+
const pkg = loadPackageJson();
|
|
30
|
+
const packagename = opt?.packagename || pkg.name;
|
|
31
|
+
const awsAccountId = opt?.awsAccountId || pkg.config?.awsAccountId || process.env.AWS_ACCOUNT_ID;
|
|
32
|
+
const tag = opt?.tag || 'latest';
|
|
33
|
+
if (!packagename) {
|
|
34
|
+
throw new Error('Missing package name. Set "name" in package.json or pass --name.');
|
|
35
|
+
}
|
|
36
|
+
if (!awsAccountId) {
|
|
37
|
+
throw new Error('Missing AWS Account ID. Set "config.awsAccountId" in package.json or AWS_ACCOUNT_ID env var.');
|
|
38
|
+
}
|
|
39
|
+
const image = `${awsAccountId}/${packagename}:${tag}`;
|
|
40
|
+
const cmd = `docker push ${image}`;
|
|
41
|
+
console.log(green(`[Docker Push]`));
|
|
42
|
+
console.log(cyan(`> ${cmd}`));
|
|
43
|
+
(0, child_process_1.execSync)(cmd, { stdio: 'inherit' });
|
|
44
|
+
console.log(green(`✓ Pushed ${image}`));
|
|
45
|
+
}
|
|
46
|
+
const USAGE = `
|
|
47
|
+
Usage:
|
|
48
|
+
ztechno-cli-push [options]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--name <name> Override image name (default: package.json "name")
|
|
52
|
+
--account <id> Override AWS Account ID (default: package.json "config.awsAccountId")
|
|
53
|
+
--tag <tag> Override tag (default: "latest")
|
|
54
|
+
-h, --help Show this help
|
|
55
|
+
|
|
56
|
+
package.json format:
|
|
57
|
+
{
|
|
58
|
+
"name": "my-image",
|
|
59
|
+
"config": {
|
|
60
|
+
"awsAccountId": "00028463827"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
`.trim();
|
|
64
|
+
if (require.main === module) {
|
|
65
|
+
try {
|
|
66
|
+
const args = process.argv.slice(2);
|
|
67
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
68
|
+
console.log(USAGE);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
const getArg = (flag) => {
|
|
72
|
+
const idx = args.indexOf(flag);
|
|
73
|
+
return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
74
|
+
};
|
|
75
|
+
dockerPush({
|
|
76
|
+
packagename: getArg('--name'),
|
|
77
|
+
awsAccountId: getArg('--account'),
|
|
78
|
+
tag: getArg('--tag'),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
console.error(red(`✗ Error: ${err.message}`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface DockerUpdateOptions {
|
|
3
|
+
/** The hostname of the Docker host */
|
|
4
|
+
hostname: string;
|
|
5
|
+
/** The package/image name */
|
|
6
|
+
packagename: string;
|
|
7
|
+
/** The port number */
|
|
8
|
+
port: string | number;
|
|
9
|
+
/** Optional array of volume mappings */
|
|
10
|
+
volumes?: string[];
|
|
11
|
+
/** Optional logger (defaults to silent) */
|
|
12
|
+
c?: {
|
|
13
|
+
log: (...args: any[]) => void;
|
|
14
|
+
error: (...args: any[]) => void;
|
|
15
|
+
};
|
|
16
|
+
/** Request timeout in ms (default: 30000) */
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface DockerUpdateResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
message?: string;
|
|
22
|
+
err?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Update a remote Docker container via the V2 Secure API
|
|
26
|
+
* @param opt - Update options
|
|
27
|
+
* @returns Promise resolving to success status and message
|
|
28
|
+
*/
|
|
29
|
+
export declare function updateDocker(opt: DockerUpdateOptions): Promise<DockerUpdateResult>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.updateDocker = updateDocker;
|
|
8
|
+
const http_1 = __importDefault(require("http"));
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const dotenv_1 = require("dotenv");
|
|
13
|
+
// ANSI color helpers
|
|
14
|
+
const red = (msg) => `\x1b[31m${msg}\x1b[0m`;
|
|
15
|
+
const green = (msg) => `\x1b[32m${msg}\x1b[0m`;
|
|
16
|
+
const cyan = (msg) => `\x1b[36m${msg}\x1b[0m`;
|
|
17
|
+
const blue = (msg) => `\x1b[34m${msg}\x1b[0m`;
|
|
18
|
+
/** Default request timeout in milliseconds (30 seconds) */
|
|
19
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
20
|
+
/**
|
|
21
|
+
* Update a remote Docker container via the V2 Secure API
|
|
22
|
+
* @param opt - Update options
|
|
23
|
+
* @returns Promise resolving to success status and message
|
|
24
|
+
*/
|
|
25
|
+
function updateDocker(opt) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const { hostname, packagename, port, volumes, c, timeout = REQUEST_TIMEOUT_MS } = opt;
|
|
28
|
+
const secretKey = process.env.ZTECHNO_API_SECRET;
|
|
29
|
+
if (!secretKey) {
|
|
30
|
+
c?.log(red('✗ Error: ZTECHNO_API_SECRET environment variable not set'));
|
|
31
|
+
return reject(new Error('ZTECHNO_API_SECRET environment variable not set'));
|
|
32
|
+
}
|
|
33
|
+
// Generate timestamp and HMAC signature
|
|
34
|
+
const timestamp = Date.now().toString();
|
|
35
|
+
const volumesString = volumes && volumes.length > 0 ? volumes.join(',') : '';
|
|
36
|
+
const payload = timestamp + packagename + port + volumesString;
|
|
37
|
+
const signature = crypto_1.default.createHmac('sha256', secretKey).update(payload).digest('hex');
|
|
38
|
+
c?.log(green(`[Updating Remote Docker via V2 Secure API]`));
|
|
39
|
+
// Build query parameters with proper encoding
|
|
40
|
+
const query = new URLSearchParams({ port: String(port) });
|
|
41
|
+
if (volumesString) {
|
|
42
|
+
query.set('volumes', volumesString);
|
|
43
|
+
}
|
|
44
|
+
const options = {
|
|
45
|
+
hostname,
|
|
46
|
+
port: 7998,
|
|
47
|
+
path: `/v2/images/${encodeURIComponent(packagename)}/update?${query}`,
|
|
48
|
+
method: 'GET',
|
|
49
|
+
timeout,
|
|
50
|
+
headers: {
|
|
51
|
+
'x-timestamp': timestamp,
|
|
52
|
+
'x-signature': signature
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const req = http_1.default.request(options, (res) => {
|
|
56
|
+
let data = '';
|
|
57
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
58
|
+
res.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
const response = JSON.parse(data);
|
|
61
|
+
c?.log(cyan(`Status Code: ${res.statusCode}`));
|
|
62
|
+
c?.log(blue(`Response: ${JSON.stringify(response, null, 2)}`));
|
|
63
|
+
if (response.success) {
|
|
64
|
+
c?.log(green(`✓ ${response.message}`));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
c?.log(red(`✗ Error: ${response.err}`));
|
|
68
|
+
}
|
|
69
|
+
resolve(response);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
c?.log(red(`Failed to parse response: ${data}`));
|
|
73
|
+
c?.error(err);
|
|
74
|
+
reject(err);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
req.on('timeout', () => {
|
|
79
|
+
req.destroy();
|
|
80
|
+
const err = new Error(`Request timed out after ${timeout}ms`);
|
|
81
|
+
c?.log(red(`✗ ${err.message}`));
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
req.on('error', (err) => {
|
|
85
|
+
c?.log(red(`✗ Request failed: ${err.message}`));
|
|
86
|
+
reject(err);
|
|
87
|
+
});
|
|
88
|
+
req.end();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read the consumer's package.json from the current working directory.
|
|
93
|
+
* Returns null if not found or unreadable.
|
|
94
|
+
*/
|
|
95
|
+
function loadPackageJson() {
|
|
96
|
+
try {
|
|
97
|
+
const pkgPath = path_1.default.join(process.cwd(), 'package.json');
|
|
98
|
+
if (!fs_1.default.existsSync(pkgPath))
|
|
99
|
+
return null;
|
|
100
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Auto-detect options from .env and/or package.json in the current working directory.
|
|
108
|
+
*
|
|
109
|
+
* Resolution order for each field:
|
|
110
|
+
* 1. .env / environment variable
|
|
111
|
+
* 2. package.json (name → packagename, config.port → port, config.volumes → volumes)
|
|
112
|
+
*
|
|
113
|
+
* Required: packagename, port, ZTECHNO_API_SECRET (env only)
|
|
114
|
+
* Optional: volumes (comma-separated)
|
|
115
|
+
*/
|
|
116
|
+
function loadOptionsFromEnv() {
|
|
117
|
+
// Load .env file
|
|
118
|
+
const envPath = path_1.default.join(process.cwd(), '.env');
|
|
119
|
+
const cfg = (0, dotenv_1.config)({ path: envPath });
|
|
120
|
+
if (cfg.error && cfg.error.message && !cfg.error.message.includes('ENOENT')) {
|
|
121
|
+
throw cfg.error;
|
|
122
|
+
}
|
|
123
|
+
// Load package.json as fallback
|
|
124
|
+
const pkg = loadPackageJson();
|
|
125
|
+
const hostname = cfg.parsed?.hostname || process.env.hostname || pkg?.config?.hostname;
|
|
126
|
+
const packagename = cfg.parsed?.packagename || process.env.packagename || pkg?.name;
|
|
127
|
+
const port = cfg.parsed?.port || process.env.port || (pkg?.config?.port != null ? String(pkg.config.port) : undefined);
|
|
128
|
+
const volumesRaw = cfg.parsed?.volumes || process.env.volumes || pkg?.config?.volumes;
|
|
129
|
+
if (pkg) {
|
|
130
|
+
console.log(`Detected package.json: ${pkg.name || '(unnamed)'}`);
|
|
131
|
+
}
|
|
132
|
+
if (!hostname) {
|
|
133
|
+
throw new Error('Missing hostname. Set it in .env or environment variable.');
|
|
134
|
+
}
|
|
135
|
+
if (!packagename) {
|
|
136
|
+
throw new Error('Missing packagename. Set it in .env, environment, or package.json "name".');
|
|
137
|
+
}
|
|
138
|
+
if (!port) {
|
|
139
|
+
throw new Error('Missing port. Set it in .env, environment, or package.json "config.port".');
|
|
140
|
+
}
|
|
141
|
+
// Normalize volumes: accept string[] from package.json or comma-separated string from .env
|
|
142
|
+
let volumes;
|
|
143
|
+
if (Array.isArray(volumesRaw)) {
|
|
144
|
+
volumes = volumesRaw.filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
else if (typeof volumesRaw === 'string') {
|
|
147
|
+
volumes = volumesRaw.split(',').filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
return { hostname, packagename, port, volumes };
|
|
150
|
+
}
|
|
151
|
+
const USAGE = `
|
|
152
|
+
Usage:
|
|
153
|
+
ztechno-cli-update <packagename>:<port>
|
|
154
|
+
ztechno-cli-update (auto-detect from .env / package.json)
|
|
155
|
+
|
|
156
|
+
Auto-detection priority:
|
|
157
|
+
1. .env file → packagename, port, volumes, ZTECHNO_API_SECRET
|
|
158
|
+
2. package.json → "name" as packagename, "config.port", "config.volumes"
|
|
159
|
+
|
|
160
|
+
.env file format:
|
|
161
|
+
ZTECHNO_API_SECRET=your_secret
|
|
162
|
+
hostname=192.168.1.100
|
|
163
|
+
packagename=my-image
|
|
164
|
+
port=3000
|
|
165
|
+
volumes=/host/path:/container/path (optional, comma-separated)
|
|
166
|
+
|
|
167
|
+
package.json format:
|
|
168
|
+
{
|
|
169
|
+
"name": "my-image",
|
|
170
|
+
"config": {
|
|
171
|
+
"hostname": "192.168.1.100",
|
|
172
|
+
"port": 3000,
|
|
173
|
+
"volumes": ["/host/path:/container/path", "/logs:/app/logs"]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`.trim();
|
|
177
|
+
// Run if executed directly (node docker-update.js or npx ztechno-cli-update)
|
|
178
|
+
if (require.main === module) {
|
|
179
|
+
const main = async () => {
|
|
180
|
+
const arg = process.argv[2];
|
|
181
|
+
if (arg === '--help' || arg === '-h') {
|
|
182
|
+
console.log(USAGE);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// No argument: load from .env
|
|
186
|
+
if (arg === undefined) {
|
|
187
|
+
const { hostname, packagename, port, volumes } = loadOptionsFromEnv();
|
|
188
|
+
await updateDocker({ hostname, packagename, port, volumes, c: console });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Argument with colon: packagename:port
|
|
192
|
+
if (arg.includes(':')) {
|
|
193
|
+
const [packagename, port] = arg.split(':');
|
|
194
|
+
if (!packagename || !port) {
|
|
195
|
+
throw new Error(`Invalid argument "${arg}". Expected format: <packagename>:<port>`);
|
|
196
|
+
}
|
|
197
|
+
const { hostname } = loadOptionsFromEnv();
|
|
198
|
+
await updateDocker({ hostname, packagename, port, c: console });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.error(red(`✗ Invalid argument: "${arg}"`));
|
|
202
|
+
console.log(USAGE);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
};
|
|
205
|
+
main().catch(err => {
|
|
206
|
+
console.error(red(`✗ Error: ${err.message}`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
});
|
|
209
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ztechno_cli",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Core files for ztechno framework",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"typings": "lib/index.d.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Ivan Auda (ZTechnologies International)",
|
|
10
|
+
"files": [
|
|
11
|
+
"lib/**/*"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "jest --config jestconfig.json",
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"format": "prettier --write \"lib/**/*.ts\" \"lib/**/*.js\"",
|
|
17
|
+
"lint": "tslint --project tsconfig.json \"src/**/*.ts\"",
|
|
18
|
+
"preversion": "npm run lint",
|
|
19
|
+
"version": "npm run format && git add -A src",
|
|
20
|
+
"postversion": "git push && git push --tags",
|
|
21
|
+
"update": "npm run build && npm version patch && npm publish"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"ztechno-cli-build": "lib/scripts/docker-build.js",
|
|
25
|
+
"ztechno-cli-push": "lib/scripts/docker-push.js",
|
|
26
|
+
"ztechno-cli-update": "lib/scripts/docker-update.js",
|
|
27
|
+
"ztechno-cli-publish": "lib/scripts/docker-publish.js"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ztechno",
|
|
31
|
+
"cli"
|
|
32
|
+
],
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
35
|
+
"@types/node": "^25.3.0",
|
|
36
|
+
"dotenv": "^17.3.1",
|
|
37
|
+
"prettier": "^3.8.1",
|
|
38
|
+
"tslint": "^6.1.3",
|
|
39
|
+
"tslint-config-prettier": "^1.18.0",
|
|
40
|
+
"tslint-eslint-rules": "^5.4.0",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|