@hasna/shortlinks 0.1.1 → 0.1.3
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/README.md +12 -1
- package/dist/cli/index.js +105 -7
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +15 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +64 -3
- package/dist/local.d.ts +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,17 @@ shortlinks serve --port 8787
|
|
|
67
67
|
shortlinks doctor
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
## Local Domain Setup
|
|
71
|
+
|
|
72
|
+
Record a local mapping with the `machines` CLI and print the remaining hosts/proxy setup:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
shortlinks local setup has.na --port 8787
|
|
76
|
+
shortlinks local plan has.na --port 8787
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The command emits the `/etc/hosts` line, a Caddy reverse-proxy snippet, and certificate paths. Writing `/etc/hosts` still requires sudo on macOS.
|
|
80
|
+
|
|
70
81
|
## Custom Domains
|
|
71
82
|
|
|
72
83
|
Add as many domains as you need:
|
|
@@ -96,7 +107,7 @@ shortlinks cloudflare worker \
|
|
|
96
107
|
--origin https://shortlinks.hasna.xyz
|
|
97
108
|
```
|
|
98
109
|
|
|
99
|
-
Upsert DNS when `CLOUDFLARE_API_TOKEN` is available
|
|
110
|
+
Upsert DNS when `CLOUDFLARE_API_TOKEN` is available. Global API key auth is also supported with `CLOUDFLARE_API_KEY` plus `CLOUDFLARE_EMAIL`.
|
|
100
111
|
|
|
101
112
|
```bash
|
|
102
113
|
shortlinks cloudflare dns has.na --target shortlinks.hasna.xyz
|
package/dist/cli/index.js
CHANGED
|
@@ -2560,9 +2560,9 @@ var source_default = chalk;
|
|
|
2560
2560
|
|
|
2561
2561
|
// src/cli/index.ts
|
|
2562
2562
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2563
|
-
import { dirname as dirname3, join as
|
|
2563
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
2564
2564
|
import { fileURLToPath } from "url";
|
|
2565
|
-
import { spawnSync as
|
|
2565
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2566
2566
|
|
|
2567
2567
|
// src/store.ts
|
|
2568
2568
|
import { createHash } from "crypto";
|
|
@@ -3224,11 +3224,25 @@ SHORTLINKS_ORIGIN = "${options.origin || "https://shortlinks.example.com"}"
|
|
|
3224
3224
|
`);
|
|
3225
3225
|
return { workerPath, wranglerPath };
|
|
3226
3226
|
}
|
|
3227
|
+
function cloudflareAuthHeaders(token) {
|
|
3228
|
+
const apiToken = token || process.env.CLOUDFLARE_API_TOKEN;
|
|
3229
|
+
if (apiToken)
|
|
3230
|
+
return { authorization: `Bearer ${apiToken}` };
|
|
3231
|
+
const apiKey = process.env.CLOUDFLARE_API_KEY;
|
|
3232
|
+
const email = process.env.CLOUDFLARE_EMAIL;
|
|
3233
|
+
if (apiKey && email) {
|
|
3234
|
+
return {
|
|
3235
|
+
"x-auth-key": apiKey,
|
|
3236
|
+
"x-auth-email": email
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
throw new Error("Cloudflare auth is required: set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY plus CLOUDFLARE_EMAIL.");
|
|
3240
|
+
}
|
|
3227
3241
|
async function cloudflareRequest(token, path, init = {}) {
|
|
3228
3242
|
const response = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
|
|
3229
3243
|
...init,
|
|
3230
3244
|
headers: {
|
|
3231
|
-
|
|
3245
|
+
...cloudflareAuthHeaders(token),
|
|
3232
3246
|
"content-type": "application/json",
|
|
3233
3247
|
...init.headers || {}
|
|
3234
3248
|
}
|
|
@@ -3266,8 +3280,6 @@ async function upsertCloudflareDnsRecord(options) {
|
|
|
3266
3280
|
});
|
|
3267
3281
|
if (options.dryRun)
|
|
3268
3282
|
return plan;
|
|
3269
|
-
if (!token)
|
|
3270
|
-
throw new Error("CLOUDFLARE_API_TOKEN is required unless --dry-run is used.");
|
|
3271
3283
|
const zoneId = options.zoneId || await findCloudflareZoneId(plan.hostname, token);
|
|
3272
3284
|
const existing = await cloudflareRequest(token, `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(plan.hostname)}`);
|
|
3273
3285
|
const payload = JSON.stringify(plan.dnsRecord);
|
|
@@ -3312,6 +3324,54 @@ function runDomains(action, domain, options = {}) {
|
|
|
3312
3324
|
};
|
|
3313
3325
|
}
|
|
3314
3326
|
|
|
3327
|
+
// src/local.ts
|
|
3328
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3329
|
+
import { homedir as homedir2 } from "os";
|
|
3330
|
+
import { join as join4 } from "path";
|
|
3331
|
+
function createLocalSetupPlan(input) {
|
|
3332
|
+
const domain = normalizeHostname(input.domain);
|
|
3333
|
+
const targetHost = input.targetHost || "127.0.0.1";
|
|
3334
|
+
const port = input.port || 8787;
|
|
3335
|
+
const certDir = input.certDir || join4(homedir2(), ".hasna", "machines", "certs");
|
|
3336
|
+
const certPath = join4(certDir, `${domain}.pem`);
|
|
3337
|
+
const keyPath = join4(certDir, `${domain}-key.pem`);
|
|
3338
|
+
return {
|
|
3339
|
+
domain,
|
|
3340
|
+
targetHost,
|
|
3341
|
+
port,
|
|
3342
|
+
hostsEntry: `${targetHost} ${domain}`,
|
|
3343
|
+
caddySnippet: `${domain} {
|
|
3344
|
+
reverse_proxy ${targetHost}:${port}
|
|
3345
|
+
tls ${certPath} ${keyPath}
|
|
3346
|
+
}`,
|
|
3347
|
+
certPath,
|
|
3348
|
+
keyPath,
|
|
3349
|
+
machinesCommand: `machines dns add --domain ${domain} --target-host ${targetHost} --port ${port} --json`,
|
|
3350
|
+
sudoRequired: true
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
function registerMachinesDns(input) {
|
|
3354
|
+
const plan = createLocalSetupPlan(input);
|
|
3355
|
+
const args = [
|
|
3356
|
+
"dns",
|
|
3357
|
+
"add",
|
|
3358
|
+
"--domain",
|
|
3359
|
+
plan.domain,
|
|
3360
|
+
"--target-host",
|
|
3361
|
+
plan.targetHost,
|
|
3362
|
+
"--port",
|
|
3363
|
+
String(plan.port),
|
|
3364
|
+
"--json"
|
|
3365
|
+
];
|
|
3366
|
+
const result = spawnSync2("machines", args, { encoding: "utf-8" });
|
|
3367
|
+
return {
|
|
3368
|
+
command: `machines ${args.join(" ")}`,
|
|
3369
|
+
status: result.status,
|
|
3370
|
+
stdout: result.stdout || "",
|
|
3371
|
+
stderr: result.stderr || ""
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3315
3375
|
// src/pg-migrations.ts
|
|
3316
3376
|
var PG_MIGRATIONS = [
|
|
3317
3377
|
`
|
|
@@ -3388,7 +3448,7 @@ var PG_MIGRATIONS = [
|
|
|
3388
3448
|
// src/cli/index.ts
|
|
3389
3449
|
function getPackageVersion() {
|
|
3390
3450
|
try {
|
|
3391
|
-
const pkgPath =
|
|
3451
|
+
const pkgPath = join5(dirname3(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
3392
3452
|
return JSON.parse(readFileSync3(pkgPath, "utf-8")).version || "0.0.0";
|
|
3393
3453
|
} catch {
|
|
3394
3454
|
return "0.0.0";
|
|
@@ -3435,7 +3495,7 @@ function formatLink(link) {
|
|
|
3435
3495
|
return `${source_default.green(link.short_url || `${link.hostname}/${link.slug}`)} ${source_default.dim("->")} ${link.destination_url}`;
|
|
3436
3496
|
}
|
|
3437
3497
|
function commandExists(command) {
|
|
3438
|
-
const result =
|
|
3498
|
+
const result = spawnSync3("which", [command], { encoding: "utf-8" });
|
|
3439
3499
|
return result.status === 0;
|
|
3440
3500
|
}
|
|
3441
3501
|
program2.name("shortlinks").description("CLI-only shortlink manager with custom domains, click tracking, Cloudflare helpers, and cloud sync").version(getPackageVersion()).option("--db <path>", "SQLite database path").option("-j, --json", "Output JSON for agents and scripts");
|
|
@@ -3814,6 +3874,42 @@ cloudCmd.command("status").description("Show local and cloud configuration healt
|
|
|
3814
3874
|
handleError(error);
|
|
3815
3875
|
}
|
|
3816
3876
|
});
|
|
3877
|
+
var localCmd = program2.command("local").description("Local domain setup helpers");
|
|
3878
|
+
localCmd.command("plan <domain>").description("Render hosts and reverse-proxy setup for a local shortlink domain").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("-j, --json", "Output JSON").action((domain, opts) => {
|
|
3879
|
+
try {
|
|
3880
|
+
const plan = createLocalSetupPlan({
|
|
3881
|
+
domain,
|
|
3882
|
+
port: Number(opts.port),
|
|
3883
|
+
targetHost: opts.targetHost
|
|
3884
|
+
});
|
|
3885
|
+
print(plan, opts, () => console.log(JSON.stringify(plan, null, 2)));
|
|
3886
|
+
} catch (error) {
|
|
3887
|
+
handleError(error);
|
|
3888
|
+
}
|
|
3889
|
+
});
|
|
3890
|
+
localCmd.command("setup <domain>").description("Record local domain mapping with machines and print remaining sudo-only setup").option("--port <port>", "Local redirect server port", "8787").option("--target-host <host>", "Local target host", "127.0.0.1").option("--skip-machines", "Do not call machines dns add").option("-j, --json", "Output JSON").action((domain, opts) => {
|
|
3891
|
+
try {
|
|
3892
|
+
const plan = createLocalSetupPlan({
|
|
3893
|
+
domain,
|
|
3894
|
+
port: Number(opts.port),
|
|
3895
|
+
targetHost: opts.targetHost
|
|
3896
|
+
});
|
|
3897
|
+
const machines = opts.skipMachines ? null : registerMachinesDns({
|
|
3898
|
+
domain,
|
|
3899
|
+
port: Number(opts.port),
|
|
3900
|
+
targetHost: opts.targetHost
|
|
3901
|
+
});
|
|
3902
|
+
const result = { plan, machines };
|
|
3903
|
+
print(result, opts, () => {
|
|
3904
|
+
if (machines && machines.status !== 0) {
|
|
3905
|
+
console.error(source_default.yellow(machines.stderr.trim() || "machines dns add failed"));
|
|
3906
|
+
}
|
|
3907
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3908
|
+
});
|
|
3909
|
+
} catch (error) {
|
|
3910
|
+
handleError(error);
|
|
3911
|
+
}
|
|
3912
|
+
});
|
|
3817
3913
|
program2.command("doctor").description("Check local shortlinks tooling and integration readiness").option("-j, --json", "Output JSON").action((opts) => {
|
|
3818
3914
|
try {
|
|
3819
3915
|
const stats = withStore((store) => store.totalStats());
|
|
@@ -3832,6 +3928,8 @@ program2.command("doctor").description("Check local shortlinks tooling and integ
|
|
|
3832
3928
|
},
|
|
3833
3929
|
environment: {
|
|
3834
3930
|
cloudflare_api_token_present: Boolean(process.env.CLOUDFLARE_API_TOKEN),
|
|
3931
|
+
cloudflare_api_key_present: Boolean(process.env.CLOUDFLARE_API_KEY),
|
|
3932
|
+
cloudflare_email_present: Boolean(process.env.CLOUDFLARE_EMAIL),
|
|
3835
3933
|
shortlinks_origin_present: Boolean(process.env.SHORTLINKS_ORIGIN)
|
|
3836
3934
|
}
|
|
3837
3935
|
};
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare function writeWorkerFiles(options?: {
|
|
|
36
36
|
workerPath: string;
|
|
37
37
|
wranglerPath: string;
|
|
38
38
|
};
|
|
39
|
-
export declare function findCloudflareZoneId(hostname: string, token
|
|
39
|
+
export declare function findCloudflareZoneId(hostname: string, token?: string): Promise<string>;
|
|
40
40
|
export declare function upsertCloudflareDnsRecord(options: CloudflareDnsOptions): Promise<CloudflareSetupPlan | {
|
|
41
41
|
id: string;
|
|
42
42
|
action: "created" | "updated";
|
package/dist/cloudflare.js
CHANGED
|
@@ -87,11 +87,25 @@ SHORTLINKS_ORIGIN = "${options.origin || "https://shortlinks.example.com"}"
|
|
|
87
87
|
`);
|
|
88
88
|
return { workerPath, wranglerPath };
|
|
89
89
|
}
|
|
90
|
+
function cloudflareAuthHeaders(token) {
|
|
91
|
+
const apiToken = token || process.env.CLOUDFLARE_API_TOKEN;
|
|
92
|
+
if (apiToken)
|
|
93
|
+
return { authorization: `Bearer ${apiToken}` };
|
|
94
|
+
const apiKey = process.env.CLOUDFLARE_API_KEY;
|
|
95
|
+
const email = process.env.CLOUDFLARE_EMAIL;
|
|
96
|
+
if (apiKey && email) {
|
|
97
|
+
return {
|
|
98
|
+
"x-auth-key": apiKey,
|
|
99
|
+
"x-auth-email": email
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
throw new Error("Cloudflare auth is required: set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY plus CLOUDFLARE_EMAIL.");
|
|
103
|
+
}
|
|
90
104
|
async function cloudflareRequest(token, path, init = {}) {
|
|
91
105
|
const response = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
|
|
92
106
|
...init,
|
|
93
107
|
headers: {
|
|
94
|
-
|
|
108
|
+
...cloudflareAuthHeaders(token),
|
|
95
109
|
"content-type": "application/json",
|
|
96
110
|
...init.headers || {}
|
|
97
111
|
}
|
|
@@ -129,8 +143,6 @@ async function upsertCloudflareDnsRecord(options) {
|
|
|
129
143
|
});
|
|
130
144
|
if (options.dryRun)
|
|
131
145
|
return plan;
|
|
132
|
-
if (!token)
|
|
133
|
-
throw new Error("CLOUDFLARE_API_TOKEN is required unless --dry-run is used.");
|
|
134
146
|
const zoneId = options.zoneId || await findCloudflareZoneId(plan.hostname, token);
|
|
135
147
|
const existing = await cloudflareRequest(token, `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(plan.hostname)}`);
|
|
136
148
|
const payload = JSON.stringify(plan.dnsRecord);
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { ShortlinksDatabase, SQLITE_MIGRATIONS, makeId, now } from "./database.j
|
|
|
2
2
|
export { ShortlinksStore } from "./store.js";
|
|
3
3
|
export { createShortlinksHandler, serveShortlinks } from "./server.js";
|
|
4
4
|
export { createCloudflarePlan, generateWorkerScript, writeWorkerFiles, upsertCloudflareDnsRecord } from "./cloudflare.js";
|
|
5
|
+
export { createLocalSetupPlan, registerMachinesDns } from "./local.js";
|
|
5
6
|
export { PG_MIGRATIONS } from "./pg-migrations.js";
|
|
6
7
|
export { formatShortUrl, getConfigPath, getDataDir, getDatabasePath, loadConfig, normalizeHostname, saveConfig } from "./config.js";
|
|
7
8
|
export { normalizeSlug, randomToken } from "./slug.js";
|
package/dist/index.js
CHANGED
|
@@ -656,11 +656,25 @@ SHORTLINKS_ORIGIN = "${options.origin || "https://shortlinks.example.com"}"
|
|
|
656
656
|
`);
|
|
657
657
|
return { workerPath, wranglerPath };
|
|
658
658
|
}
|
|
659
|
+
function cloudflareAuthHeaders(token) {
|
|
660
|
+
const apiToken = token || process.env.CLOUDFLARE_API_TOKEN;
|
|
661
|
+
if (apiToken)
|
|
662
|
+
return { authorization: `Bearer ${apiToken}` };
|
|
663
|
+
const apiKey = process.env.CLOUDFLARE_API_KEY;
|
|
664
|
+
const email = process.env.CLOUDFLARE_EMAIL;
|
|
665
|
+
if (apiKey && email) {
|
|
666
|
+
return {
|
|
667
|
+
"x-auth-key": apiKey,
|
|
668
|
+
"x-auth-email": email
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
throw new Error("Cloudflare auth is required: set CLOUDFLARE_API_TOKEN, or CLOUDFLARE_API_KEY plus CLOUDFLARE_EMAIL.");
|
|
672
|
+
}
|
|
659
673
|
async function cloudflareRequest(token, path, init = {}) {
|
|
660
674
|
const response = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
|
|
661
675
|
...init,
|
|
662
676
|
headers: {
|
|
663
|
-
|
|
677
|
+
...cloudflareAuthHeaders(token),
|
|
664
678
|
"content-type": "application/json",
|
|
665
679
|
...init.headers || {}
|
|
666
680
|
}
|
|
@@ -698,8 +712,6 @@ async function upsertCloudflareDnsRecord(options) {
|
|
|
698
712
|
});
|
|
699
713
|
if (options.dryRun)
|
|
700
714
|
return plan;
|
|
701
|
-
if (!token)
|
|
702
|
-
throw new Error("CLOUDFLARE_API_TOKEN is required unless --dry-run is used.");
|
|
703
715
|
const zoneId = options.zoneId || await findCloudflareZoneId(plan.hostname, token);
|
|
704
716
|
const existing = await cloudflareRequest(token, `/zones/${zoneId}/dns_records?type=CNAME&name=${encodeURIComponent(plan.hostname)}`);
|
|
705
717
|
const payload = JSON.stringify(plan.dnsRecord);
|
|
@@ -716,6 +728,53 @@ async function upsertCloudflareDnsRecord(options) {
|
|
|
716
728
|
});
|
|
717
729
|
return { id: created.id, action: "created" };
|
|
718
730
|
}
|
|
731
|
+
// src/local.ts
|
|
732
|
+
import { spawnSync } from "child_process";
|
|
733
|
+
import { homedir as homedir2 } from "os";
|
|
734
|
+
import { join as join4 } from "path";
|
|
735
|
+
function createLocalSetupPlan(input) {
|
|
736
|
+
const domain = normalizeHostname(input.domain);
|
|
737
|
+
const targetHost = input.targetHost || "127.0.0.1";
|
|
738
|
+
const port = input.port || 8787;
|
|
739
|
+
const certDir = input.certDir || join4(homedir2(), ".hasna", "machines", "certs");
|
|
740
|
+
const certPath = join4(certDir, `${domain}.pem`);
|
|
741
|
+
const keyPath = join4(certDir, `${domain}-key.pem`);
|
|
742
|
+
return {
|
|
743
|
+
domain,
|
|
744
|
+
targetHost,
|
|
745
|
+
port,
|
|
746
|
+
hostsEntry: `${targetHost} ${domain}`,
|
|
747
|
+
caddySnippet: `${domain} {
|
|
748
|
+
reverse_proxy ${targetHost}:${port}
|
|
749
|
+
tls ${certPath} ${keyPath}
|
|
750
|
+
}`,
|
|
751
|
+
certPath,
|
|
752
|
+
keyPath,
|
|
753
|
+
machinesCommand: `machines dns add --domain ${domain} --target-host ${targetHost} --port ${port} --json`,
|
|
754
|
+
sudoRequired: true
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function registerMachinesDns(input) {
|
|
758
|
+
const plan = createLocalSetupPlan(input);
|
|
759
|
+
const args = [
|
|
760
|
+
"dns",
|
|
761
|
+
"add",
|
|
762
|
+
"--domain",
|
|
763
|
+
plan.domain,
|
|
764
|
+
"--target-host",
|
|
765
|
+
plan.targetHost,
|
|
766
|
+
"--port",
|
|
767
|
+
String(plan.port),
|
|
768
|
+
"--json"
|
|
769
|
+
];
|
|
770
|
+
const result = spawnSync("machines", args, { encoding: "utf-8" });
|
|
771
|
+
return {
|
|
772
|
+
command: `machines ${args.join(" ")}`,
|
|
773
|
+
status: result.status,
|
|
774
|
+
stdout: result.stdout || "",
|
|
775
|
+
stderr: result.stderr || ""
|
|
776
|
+
};
|
|
777
|
+
}
|
|
719
778
|
// src/pg-migrations.ts
|
|
720
779
|
var PG_MIGRATIONS = [
|
|
721
780
|
`
|
|
@@ -793,6 +852,7 @@ export {
|
|
|
793
852
|
upsertCloudflareDnsRecord,
|
|
794
853
|
serveShortlinks,
|
|
795
854
|
saveConfig,
|
|
855
|
+
registerMachinesDns,
|
|
796
856
|
randomToken,
|
|
797
857
|
now,
|
|
798
858
|
normalizeSlug,
|
|
@@ -805,6 +865,7 @@ export {
|
|
|
805
865
|
generateWorkerScript,
|
|
806
866
|
formatShortUrl,
|
|
807
867
|
createShortlinksHandler,
|
|
868
|
+
createLocalSetupPlan,
|
|
808
869
|
createCloudflarePlan,
|
|
809
870
|
ShortlinksStore,
|
|
810
871
|
ShortlinksDatabase,
|
package/dist/local.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface LocalSetupPlan {
|
|
2
|
+
domain: string;
|
|
3
|
+
targetHost: string;
|
|
4
|
+
port: number;
|
|
5
|
+
hostsEntry: string;
|
|
6
|
+
caddySnippet: string;
|
|
7
|
+
certPath: string;
|
|
8
|
+
keyPath: string;
|
|
9
|
+
machinesCommand: string;
|
|
10
|
+
sudoRequired: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createLocalSetupPlan(input: {
|
|
13
|
+
domain: string;
|
|
14
|
+
port?: number;
|
|
15
|
+
targetHost?: string;
|
|
16
|
+
certDir?: string;
|
|
17
|
+
}): LocalSetupPlan;
|
|
18
|
+
export declare function registerMachinesDns(input: {
|
|
19
|
+
domain: string;
|
|
20
|
+
port?: number;
|
|
21
|
+
targetHost?: string;
|
|
22
|
+
}): {
|
|
23
|
+
command: string;
|
|
24
|
+
status: number | null;
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
};
|
package/package.json
CHANGED