@deaduchiha/orbito 0.1.1 → 0.1.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/config.schema.json +99 -2
- package/dist/orbito.js +1591 -128
- package/package.json +1 -1
package/dist/orbito.js
CHANGED
|
@@ -2118,9 +2118,9 @@ __exportCjs(exports_commander, {
|
|
|
2118
2118
|
import process2 from "node:process";
|
|
2119
2119
|
|
|
2120
2120
|
// src/cli.ts
|
|
2121
|
-
import
|
|
2122
|
-
import
|
|
2123
|
-
import
|
|
2121
|
+
import crypto2 from "node:crypto";
|
|
2122
|
+
import fs4 from "node:fs";
|
|
2123
|
+
import path4 from "node:path";
|
|
2124
2124
|
|
|
2125
2125
|
// ../../node_modules/.bun/commander@14.0.3/node_modules/commander/index.js
|
|
2126
2126
|
var { Argument } = require_argument();
|
|
@@ -2154,37 +2154,6 @@ var {
|
|
|
2154
2154
|
Option: Option2,
|
|
2155
2155
|
Help: Help2
|
|
2156
2156
|
} = exports_commander;
|
|
2157
|
-
// package.json
|
|
2158
|
-
var package_default = {
|
|
2159
|
-
name: "@deaduchiha/orbito",
|
|
2160
|
-
version: "0.1.1",
|
|
2161
|
-
description: "Deploy, follow logs and check migrations of apps on an Orbito server",
|
|
2162
|
-
license: "UNLICENSED",
|
|
2163
|
-
bin: {
|
|
2164
|
-
orbito: "dist/orbito.js"
|
|
2165
|
-
},
|
|
2166
|
-
files: [
|
|
2167
|
-
"dist",
|
|
2168
|
-
"config.schema.json"
|
|
2169
|
-
],
|
|
2170
|
-
type: "module",
|
|
2171
|
-
exports: {
|
|
2172
|
-
"./config.schema.json": "./config.schema.json"
|
|
2173
|
-
},
|
|
2174
|
-
scripts: {
|
|
2175
|
-
build: "bun build src/main.ts --target node --outfile dist/orbito.js && bun run schema",
|
|
2176
|
-
schema: "bun scripts/write-schema.ts",
|
|
2177
|
-
test: "bun test",
|
|
2178
|
-
prepack: "bun run build"
|
|
2179
|
-
},
|
|
2180
|
-
engines: {
|
|
2181
|
-
node: ">=20"
|
|
2182
|
-
},
|
|
2183
|
-
devDependencies: {
|
|
2184
|
-
commander: "^14.0.0",
|
|
2185
|
-
zod: "^4.6.7"
|
|
2186
|
-
}
|
|
2187
|
-
};
|
|
2188
2157
|
|
|
2189
2158
|
// ../../node_modules/.bun/zod@4.6.7/node_modules/zod/v4/core/util.js
|
|
2190
2159
|
function getEnumValues(entries) {
|
|
@@ -3173,6 +3142,7 @@ function datetime(args) {
|
|
|
3173
3142
|
return new RegExp(`^${dateSource}T(?:${timeRegex})$`);
|
|
3174
3143
|
}
|
|
3175
3144
|
var anyString = /^[\s\S]{0,}$/;
|
|
3145
|
+
var integer = /^-?\d+$/;
|
|
3176
3146
|
var number = /^-?\d+(?:\.\d+)?$/;
|
|
3177
3147
|
var boolean = /^(?:true|false)$/i;
|
|
3178
3148
|
var lowercase = /^[^A-Z]*$/;
|
|
@@ -4643,6 +4613,161 @@ function handleIntersectionResults(result, left, right) {
|
|
|
4643
4613
|
result.value = merged.data;
|
|
4644
4614
|
return result;
|
|
4645
4615
|
}
|
|
4616
|
+
var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
4617
|
+
$ZodType.init(inst, def);
|
|
4618
|
+
const memo = globalConfig.memoizer;
|
|
4619
|
+
memo?.attach(inst);
|
|
4620
|
+
inst._zod.parse = (payload, ctx) => {
|
|
4621
|
+
const input = payload.value;
|
|
4622
|
+
if (!isPlainObject(input)) {
|
|
4623
|
+
payload.issues.push({
|
|
4624
|
+
expected: "record",
|
|
4625
|
+
code: "invalid_type",
|
|
4626
|
+
input,
|
|
4627
|
+
inst
|
|
4628
|
+
});
|
|
4629
|
+
return payload;
|
|
4630
|
+
}
|
|
4631
|
+
const proms = [];
|
|
4632
|
+
const values = def.keyType._zod.values;
|
|
4633
|
+
if (values && !def.partial) {
|
|
4634
|
+
payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};
|
|
4635
|
+
const recordKeys = new Set;
|
|
4636
|
+
for (const key of values) {
|
|
4637
|
+
if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
|
|
4638
|
+
recordKeys.add(typeof key === "number" ? key.toString() : key);
|
|
4639
|
+
if (key === "__proto__")
|
|
4640
|
+
continue;
|
|
4641
|
+
const keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
|
|
4642
|
+
if (keyResult instanceof Promise) {
|
|
4643
|
+
throw new Error("Async schemas not supported in object keys currently");
|
|
4644
|
+
}
|
|
4645
|
+
if (keyResult.issues.length) {
|
|
4646
|
+
payload.issues.push({
|
|
4647
|
+
code: "invalid_key",
|
|
4648
|
+
origin: "record",
|
|
4649
|
+
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4650
|
+
input: key,
|
|
4651
|
+
path: [key],
|
|
4652
|
+
inst
|
|
4653
|
+
});
|
|
4654
|
+
continue;
|
|
4655
|
+
}
|
|
4656
|
+
const outKey = keyResult.value;
|
|
4657
|
+
if (outKey === "__proto__")
|
|
4658
|
+
continue;
|
|
4659
|
+
const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
|
|
4660
|
+
if (result instanceof Promise) {
|
|
4661
|
+
proms.push(result.then((result) => {
|
|
4662
|
+
if (result.issues.length) {
|
|
4663
|
+
payload.issues.push(...prefixIssues(key, result.issues));
|
|
4664
|
+
}
|
|
4665
|
+
payload.value[outKey] = result.value;
|
|
4666
|
+
}));
|
|
4667
|
+
} else {
|
|
4668
|
+
if (result.issues.length) {
|
|
4669
|
+
payload.issues.push(...prefixIssues(key, result.issues));
|
|
4670
|
+
}
|
|
4671
|
+
payload.value[outKey] = result.value;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
let unrecognized;
|
|
4676
|
+
for (const key in input) {
|
|
4677
|
+
if (!recordKeys.has(key)) {
|
|
4678
|
+
if (def.mode === "loose") {
|
|
4679
|
+
if (key === "__proto__")
|
|
4680
|
+
continue;
|
|
4681
|
+
payload.value[key] = input[key];
|
|
4682
|
+
} else {
|
|
4683
|
+
unrecognized = unrecognized ?? [];
|
|
4684
|
+
unrecognized.push(key);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
if (unrecognized && unrecognized.length > 0) {
|
|
4689
|
+
payload.issues.push({
|
|
4690
|
+
code: "unrecognized_keys",
|
|
4691
|
+
input,
|
|
4692
|
+
inst,
|
|
4693
|
+
keys: unrecognized,
|
|
4694
|
+
continue: true
|
|
4695
|
+
});
|
|
4696
|
+
}
|
|
4697
|
+
} else {
|
|
4698
|
+
payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};
|
|
4699
|
+
let unrecognized;
|
|
4700
|
+
for (const key of Reflect.ownKeys(input)) {
|
|
4701
|
+
if (key === "__proto__")
|
|
4702
|
+
continue;
|
|
4703
|
+
if (!Object.prototype.propertyIsEnumerable.call(input, key))
|
|
4704
|
+
continue;
|
|
4705
|
+
let keyResult = def.keyType._zod.run({ value: key, issues: [] }, ctx);
|
|
4706
|
+
if (keyResult instanceof Promise) {
|
|
4707
|
+
throw new Error("Async schemas not supported in object keys currently");
|
|
4708
|
+
}
|
|
4709
|
+
const checkNumericKey = typeof key === "string" && number.test(key) && keyResult.issues.length;
|
|
4710
|
+
if (checkNumericKey) {
|
|
4711
|
+
const retryResult = def.keyType._zod.run({ value: Number(key), issues: [] }, ctx);
|
|
4712
|
+
if (retryResult instanceof Promise) {
|
|
4713
|
+
throw new Error("Async schemas not supported in object keys currently");
|
|
4714
|
+
}
|
|
4715
|
+
if (retryResult.issues.length === 0) {
|
|
4716
|
+
keyResult = retryResult;
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (keyResult.issues.length) {
|
|
4720
|
+
if (def.mode === "loose") {
|
|
4721
|
+
payload.value[key] = input[key];
|
|
4722
|
+
} else if (values) {
|
|
4723
|
+
unrecognized = unrecognized ?? [];
|
|
4724
|
+
unrecognized.push(key);
|
|
4725
|
+
} else {
|
|
4726
|
+
payload.issues.push({
|
|
4727
|
+
code: "invalid_key",
|
|
4728
|
+
origin: "record",
|
|
4729
|
+
issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
|
|
4730
|
+
input: key,
|
|
4731
|
+
path: [key],
|
|
4732
|
+
inst
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
continue;
|
|
4736
|
+
}
|
|
4737
|
+
const outKey = keyResult.value;
|
|
4738
|
+
if (outKey === "__proto__")
|
|
4739
|
+
continue;
|
|
4740
|
+
const result = def.valueType._zod.run({ value: input[key], issues: [] }, ctx);
|
|
4741
|
+
if (result instanceof Promise) {
|
|
4742
|
+
proms.push(result.then((result) => {
|
|
4743
|
+
if (result.issues.length) {
|
|
4744
|
+
payload.issues.push(...prefixIssues(key, result.issues));
|
|
4745
|
+
}
|
|
4746
|
+
payload.value[outKey] = result.value;
|
|
4747
|
+
}));
|
|
4748
|
+
} else {
|
|
4749
|
+
if (result.issues.length) {
|
|
4750
|
+
payload.issues.push(...prefixIssues(key, result.issues));
|
|
4751
|
+
}
|
|
4752
|
+
payload.value[outKey] = result.value;
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
if (unrecognized && unrecognized.length > 0) {
|
|
4756
|
+
payload.issues.push({
|
|
4757
|
+
code: "unrecognized_keys",
|
|
4758
|
+
input,
|
|
4759
|
+
inst,
|
|
4760
|
+
keys: unrecognized,
|
|
4761
|
+
continue: true
|
|
4762
|
+
});
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
if (proms.length) {
|
|
4766
|
+
return Promise.all(proms).then(() => payload);
|
|
4767
|
+
}
|
|
4768
|
+
return payload;
|
|
4769
|
+
};
|
|
4770
|
+
});
|
|
4646
4771
|
var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
4647
4772
|
$ZodType.init(inst, def);
|
|
4648
4773
|
const values = getEnumValues(def.entries);
|
|
@@ -6683,7 +6808,112 @@ var intersectionProcessor = (schema, ctx, json, params) => {
|
|
|
6683
6808
|
json.allOf = allOf;
|
|
6684
6809
|
ctx.intersections.push(allOf);
|
|
6685
6810
|
};
|
|
6811
|
+
function stringifyKeyNames(bySchema, json, visited) {
|
|
6812
|
+
if (json.$ref) {
|
|
6813
|
+
if (visited.has(json))
|
|
6814
|
+
return json;
|
|
6815
|
+
visited.add(json);
|
|
6816
|
+
const def = bySchema.get(json)?.def;
|
|
6817
|
+
if (!def)
|
|
6818
|
+
return json;
|
|
6819
|
+
const inlined = stringifyKeyNames(bySchema, def, visited);
|
|
6820
|
+
return inlined === def ? json : inlined;
|
|
6821
|
+
}
|
|
6822
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
6823
|
+
const branches = json[keyword];
|
|
6824
|
+
if (!Array.isArray(branches))
|
|
6825
|
+
continue;
|
|
6826
|
+
const mapped = branches.map((branch) => stringifyKeyNames(bySchema, branch, visited));
|
|
6827
|
+
if (mapped.some((branch, i) => branch !== branches[i]))
|
|
6828
|
+
json = { ...json, [keyword]: mapped };
|
|
6829
|
+
}
|
|
6830
|
+
const types = Array.isArray(json.type) ? json.type : [json.type];
|
|
6831
|
+
const numericType = !types.includes("string") && types.some((t) => t === "number" || t === "integer");
|
|
6832
|
+
const values = json.enum ?? (json.const !== undefined ? [json.const] : undefined);
|
|
6833
|
+
if (!numericType && !values?.some((v) => typeof v === "number"))
|
|
6834
|
+
return json;
|
|
6835
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, format, id, ...rest } = json;
|
|
6836
|
+
if (rest.enum)
|
|
6837
|
+
rest.enum = rest.enum.map((v) => typeof v === "number" ? String(v) : v);
|
|
6838
|
+
else if (typeof rest.const === "number")
|
|
6839
|
+
rest.const = String(rest.const);
|
|
6840
|
+
if (!numericType)
|
|
6841
|
+
return rest;
|
|
6842
|
+
rest.type = "string";
|
|
6843
|
+
if (!values)
|
|
6844
|
+
rest.pattern = (types.includes("number") ? number : integer).source;
|
|
6845
|
+
return rest;
|
|
6846
|
+
}
|
|
6686
6847
|
var pendingRecords = new WeakMap;
|
|
6848
|
+
function rewriteKeyNames(ctx) {
|
|
6849
|
+
const bySchema = new Map;
|
|
6850
|
+
for (const entry of ctx.seen.values()) {
|
|
6851
|
+
if (entry.def && !bySchema.has(entry.schema))
|
|
6852
|
+
bySchema.set(entry.schema, entry);
|
|
6853
|
+
}
|
|
6854
|
+
const rewrites = new Map;
|
|
6855
|
+
for (const record of pendingRecords.get(ctx) ?? []) {
|
|
6856
|
+
const seen = ctx.seen.get(record);
|
|
6857
|
+
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
6858
|
+
if (!names || names === true || rewrites.has(names))
|
|
6859
|
+
continue;
|
|
6860
|
+
const rewritten = stringifyKeyNames(bySchema, names, new Set);
|
|
6861
|
+
if (rewritten !== names)
|
|
6862
|
+
rewrites.set(names, rewritten);
|
|
6863
|
+
}
|
|
6864
|
+
if (!rewrites.size)
|
|
6865
|
+
return;
|
|
6866
|
+
for (const entry of ctx.seen.values()) {
|
|
6867
|
+
for (const carrier of [entry.schema, entry.def]) {
|
|
6868
|
+
const rewritten = carrier && rewrites.get(carrier.propertyNames);
|
|
6869
|
+
if (rewritten)
|
|
6870
|
+
carrier.propertyNames = rewritten;
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
var recordProcessor = (schema, ctx, _json, params) => {
|
|
6875
|
+
const json = _json;
|
|
6876
|
+
const def = schema._zod.def;
|
|
6877
|
+
json.type = "object";
|
|
6878
|
+
const keyType = def.keyType;
|
|
6879
|
+
const patterns = aggregateChecks(keyType).patterns;
|
|
6880
|
+
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
6881
|
+
const valueSchema = processSchema(def.valueType, ctx, {
|
|
6882
|
+
...params,
|
|
6883
|
+
path: [...params.path, "patternProperties", "*"]
|
|
6884
|
+
});
|
|
6885
|
+
json.patternProperties = {};
|
|
6886
|
+
for (const pattern of patterns) {
|
|
6887
|
+
assignProp(json.patternProperties, exactPattern(pattern).source, valueSchema);
|
|
6888
|
+
}
|
|
6889
|
+
} else {
|
|
6890
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
6891
|
+
json.propertyNames = processSchema(def.keyType, ctx, {
|
|
6892
|
+
...params,
|
|
6893
|
+
path: [...params.path, "propertyNames"]
|
|
6894
|
+
});
|
|
6895
|
+
let pending = pendingRecords.get(ctx);
|
|
6896
|
+
if (!pending) {
|
|
6897
|
+
pending = [];
|
|
6898
|
+
pendingRecords.set(ctx, pending);
|
|
6899
|
+
ctx.deferred.push(() => rewriteKeyNames(ctx));
|
|
6900
|
+
}
|
|
6901
|
+
pending.push(schema);
|
|
6902
|
+
}
|
|
6903
|
+
json.additionalProperties = processSchema(def.valueType, ctx, {
|
|
6904
|
+
...params,
|
|
6905
|
+
path: [...params.path, "additionalProperties"]
|
|
6906
|
+
});
|
|
6907
|
+
}
|
|
6908
|
+
const keyValues = keyType._zod.values;
|
|
6909
|
+
const omittableOnInput = ctx.io === "input" && inputOptin(def.valueType) !== undefined;
|
|
6910
|
+
if (keyValues && !def.partial && !omittableOnInput) {
|
|
6911
|
+
const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
|
|
6912
|
+
if (validKeyValues.length > 0) {
|
|
6913
|
+
json.required = validKeyValues.map(String);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
};
|
|
6687
6917
|
var nullableProcessor = (schema, ctx, json, params) => {
|
|
6688
6918
|
const def = schema._zod.def;
|
|
6689
6919
|
const inner = processSchema(def.innerType, ctx, params);
|
|
@@ -7466,6 +7696,30 @@ function intersection(left, right) {
|
|
|
7466
7696
|
right
|
|
7467
7697
|
});
|
|
7468
7698
|
}
|
|
7699
|
+
var ZodRecord = /* @__PURE__ */ $constructor("ZodRecord", (inst, def) => {
|
|
7700
|
+
_ensureDefaultMemoizer();
|
|
7701
|
+
$ZodRecord.init(inst, def);
|
|
7702
|
+
ZodType.init(inst, def);
|
|
7703
|
+
inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);
|
|
7704
|
+
inst.keyType = def.keyType;
|
|
7705
|
+
inst.valueType = def.valueType;
|
|
7706
|
+
});
|
|
7707
|
+
function record(keyType, valueType, params) {
|
|
7708
|
+
if (!valueType || !valueType._zod) {
|
|
7709
|
+
return new ZodRecord({
|
|
7710
|
+
type: "record",
|
|
7711
|
+
keyType: string2(),
|
|
7712
|
+
valueType: keyType,
|
|
7713
|
+
...normalizeParams(valueType)
|
|
7714
|
+
});
|
|
7715
|
+
}
|
|
7716
|
+
return new ZodRecord({
|
|
7717
|
+
type: "record",
|
|
7718
|
+
keyType,
|
|
7719
|
+
valueType,
|
|
7720
|
+
...normalizeParams(params)
|
|
7721
|
+
});
|
|
7722
|
+
}
|
|
7469
7723
|
var ZodEnum = /* @__PURE__ */ $constructor("ZodEnum", (inst, def) => {
|
|
7470
7724
|
$ZodEnum.init(inst, def);
|
|
7471
7725
|
ZodType.init(inst, def);
|
|
@@ -7704,23 +7958,108 @@ function refine(fn, _params = {}) {
|
|
|
7704
7958
|
function superRefine(fn, params) {
|
|
7705
7959
|
return _superRefine(fn, params);
|
|
7706
7960
|
}
|
|
7961
|
+
// package.json
|
|
7962
|
+
var package_default = {
|
|
7963
|
+
name: "@deaduchiha/orbito",
|
|
7964
|
+
version: "0.1.2",
|
|
7965
|
+
description: "Deploy, follow logs and check migrations of apps on an Orbito server",
|
|
7966
|
+
license: "UNLICENSED",
|
|
7967
|
+
bin: {
|
|
7968
|
+
orbito: "dist/orbito.js"
|
|
7969
|
+
},
|
|
7970
|
+
files: [
|
|
7971
|
+
"dist",
|
|
7972
|
+
"config.schema.json"
|
|
7973
|
+
],
|
|
7974
|
+
type: "module",
|
|
7975
|
+
exports: {
|
|
7976
|
+
"./config.schema.json": "./config.schema.json"
|
|
7977
|
+
},
|
|
7978
|
+
scripts: {
|
|
7979
|
+
build: "bun build src/main.ts --target node --outfile dist/orbito.js && bun run schema",
|
|
7980
|
+
schema: "bun scripts/write-schema.ts",
|
|
7981
|
+
test: "bun test",
|
|
7982
|
+
prepack: "bun run build"
|
|
7983
|
+
},
|
|
7984
|
+
engines: {
|
|
7985
|
+
node: ">=20"
|
|
7986
|
+
},
|
|
7987
|
+
devDependencies: {
|
|
7988
|
+
commander: "^14.0.0",
|
|
7989
|
+
zod: "^4.6.7"
|
|
7990
|
+
}
|
|
7991
|
+
};
|
|
7992
|
+
|
|
7707
7993
|
// src/contracts.ts
|
|
7708
7994
|
var API_VERSION = 1;
|
|
7709
7995
|
var API_PREFIX = "/api/cli/v1";
|
|
7710
7996
|
var CLI_CLIENT_ID = "orbito-cli";
|
|
7711
7997
|
var APP_ID = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
7712
7998
|
var LEVELS = ["read", "deploy", "migrate"];
|
|
7999
|
+
var DesiredShape = {
|
|
8000
|
+
preset: string2().max(40).optional(),
|
|
8001
|
+
branch: string2().max(100).optional(),
|
|
8002
|
+
install: string2().max(10).optional(),
|
|
8003
|
+
build: string2().max(10).optional(),
|
|
8004
|
+
migrate: string2().max(10).optional(),
|
|
8005
|
+
restart: string2().max(10).optional(),
|
|
8006
|
+
pm2Name: string2().max(64).optional(),
|
|
8007
|
+
healthPath: string2().max(201).optional(),
|
|
8008
|
+
layout: string2().max(10).optional(),
|
|
8009
|
+
shared: array(string2().max(200)).max(50).optional(),
|
|
8010
|
+
vars: record(string2().max(64), string2().max(4096)).optional(),
|
|
8011
|
+
secrets: strictObject({ required: array(string2().max(64)).max(100) }).optional()
|
|
8012
|
+
};
|
|
8013
|
+
var Desired = strictObject(DesiredShape);
|
|
7713
8014
|
var ConfigSchema = strictObject({
|
|
7714
8015
|
$schema: string2().optional(),
|
|
7715
|
-
version: literal(1,
|
|
8016
|
+
version: union([literal(1), literal(2)], {
|
|
8017
|
+
error: "Unsupported orbito.json version: this CLI reads versions 1 and 2"
|
|
8018
|
+
}),
|
|
7716
8019
|
server: url({ protocol: /^https?$/, error: "server must be an http(s) URL" }),
|
|
7717
|
-
appId: string2().regex(APP_ID, "appId must be an Orbito app id such as my-app")
|
|
8020
|
+
appId: string2().regex(APP_ID, "appId must be an Orbito app id such as my-app"),
|
|
8021
|
+
...DesiredShape,
|
|
8022
|
+
dev: strictObject({
|
|
8023
|
+
database: _enum(["sqlite", "postgres"]).optional(),
|
|
8024
|
+
postgres: string2().regex(/^\d{2}(\.\d{1,2})?$/, "postgres must be a version such as 18.6").optional()
|
|
8025
|
+
}).optional()
|
|
8026
|
+
}).refine((c) => c.version === 2 || Object.keys(c).every((k) => !(k in DesiredShape) && k !== "dev"), {
|
|
8027
|
+
error: 'Build and runtime settings need "version": 2 in orbito.json'
|
|
8028
|
+
});
|
|
8029
|
+
function desiredOf(c) {
|
|
8030
|
+
return Object.fromEntries(Object.entries(c).filter(([k]) => (k in DesiredShape)));
|
|
8031
|
+
}
|
|
8032
|
+
var ManifestPlan = object({
|
|
8033
|
+
appId: string2(),
|
|
8034
|
+
changes: array(object({ op: _enum(["add", "change"]), what: string2(), detail: string2() })),
|
|
8035
|
+
blockers: array(string2()),
|
|
8036
|
+
upToDate: boolean2(),
|
|
8037
|
+
hash: string2()
|
|
8038
|
+
});
|
|
8039
|
+
var ApplyManifest = object({ desired: Desired, hash: string2().max(80) });
|
|
8040
|
+
var UnmanagedProject = object({
|
|
8041
|
+
project: string2(),
|
|
8042
|
+
name: string2(),
|
|
8043
|
+
suggestedPort: number2().nullable(),
|
|
8044
|
+
runtimes: array(string2())
|
|
7718
8045
|
});
|
|
7719
8046
|
var Capabilities = object({
|
|
7720
8047
|
api: number2().int(),
|
|
7721
8048
|
features: object({
|
|
7722
8049
|
migrations: boolean2(),
|
|
7723
|
-
stagedReleases: boolean2()
|
|
8050
|
+
stagedReleases: boolean2(),
|
|
8051
|
+
rollbackPlan: boolean2().optional(),
|
|
8052
|
+
envOnly: boolean2().optional(),
|
|
8053
|
+
envCommands: boolean2().optional(),
|
|
8054
|
+
usage: boolean2().optional(),
|
|
8055
|
+
dbBackups: boolean2().optional(),
|
|
8056
|
+
manifest: boolean2().optional(),
|
|
8057
|
+
domains: boolean2().optional(),
|
|
8058
|
+
buckets: boolean2().optional(),
|
|
8059
|
+
envValues: boolean2().optional(),
|
|
8060
|
+
environments: boolean2().optional(),
|
|
8061
|
+
export: boolean2().optional(),
|
|
8062
|
+
licence: boolean2().optional()
|
|
7724
8063
|
})
|
|
7725
8064
|
});
|
|
7726
8065
|
var Identity = object({ kind: _enum(["user", "automation"]), label: string2() });
|
|
@@ -7773,12 +8112,100 @@ var SubmitDeploy = object({
|
|
|
7773
8112
|
sha: string2().regex(/^[0-9a-f]{40}$/),
|
|
7774
8113
|
recipeHash: string2().min(8).max(80),
|
|
7775
8114
|
idempotencyKey: string2().regex(/^[A-Za-z0-9_-]{8,80}$/),
|
|
7776
|
-
confirmMigrations: boolean2().optional()
|
|
8115
|
+
confirmMigrations: boolean2().optional(),
|
|
8116
|
+
envOnly: boolean2().optional()
|
|
7777
8117
|
});
|
|
7778
8118
|
var SubmitRollback = object({
|
|
7779
8119
|
idempotencyKey: string2().regex(/^[A-Za-z0-9_-]{8,80}$/),
|
|
7780
8120
|
deploymentId: string2().max(60).optional()
|
|
7781
8121
|
});
|
|
8122
|
+
var EnvList = array(object({
|
|
8123
|
+
name: string2(),
|
|
8124
|
+
secret: boolean2(),
|
|
8125
|
+
updatedAt: string2(),
|
|
8126
|
+
value: string2().optional()
|
|
8127
|
+
}));
|
|
8128
|
+
var SetEnv = object({
|
|
8129
|
+
name: string2().max(200),
|
|
8130
|
+
value: string2(),
|
|
8131
|
+
secret: boolean2().optional()
|
|
8132
|
+
});
|
|
8133
|
+
var AppUsage = object({
|
|
8134
|
+
appId: string2(),
|
|
8135
|
+
range: string2(),
|
|
8136
|
+
measured: boolean2(),
|
|
8137
|
+
at: string2().nullable(),
|
|
8138
|
+
memoryBytes: number2().nullable(),
|
|
8139
|
+
memoryPeakBytes: number2().nullable(),
|
|
8140
|
+
cpuPercent: number2().nullable(),
|
|
8141
|
+
pids: number2().nullable(),
|
|
8142
|
+
restarts: number2().nullable(),
|
|
8143
|
+
oomKills: number2().nullable(),
|
|
8144
|
+
diskBytes: number2().nullable(),
|
|
8145
|
+
requests: number2().nullable().optional(),
|
|
8146
|
+
errors: number2().nullable().optional(),
|
|
8147
|
+
p50Ms: number2().nullable().optional(),
|
|
8148
|
+
p95Ms: number2().nullable().optional(),
|
|
8149
|
+
alerts: array(object({ kind: string2(), message: string2(), since: string2().nullable() })).optional(),
|
|
8150
|
+
points: array(object({
|
|
8151
|
+
at: string2(),
|
|
8152
|
+
memoryBytes: number2().nullable(),
|
|
8153
|
+
cpuPercent: number2().nullable(),
|
|
8154
|
+
p95Ms: number2().nullable().optional()
|
|
8155
|
+
}))
|
|
8156
|
+
});
|
|
8157
|
+
var RestoreDump = object({
|
|
8158
|
+
file: string2().max(100),
|
|
8159
|
+
confirm: string2().max(80)
|
|
8160
|
+
});
|
|
8161
|
+
var DomainList = object({
|
|
8162
|
+
domains: array(object({ domain: string2(), stage: string2(), detail: string2() })),
|
|
8163
|
+
applied: array(string2()).optional(),
|
|
8164
|
+
note: string2().nullable().optional()
|
|
8165
|
+
});
|
|
8166
|
+
var AddConnection = object({
|
|
8167
|
+
name: string2().max(40),
|
|
8168
|
+
endpoint: string2().max(300),
|
|
8169
|
+
region: string2().max(60),
|
|
8170
|
+
accessKey: string2().max(300),
|
|
8171
|
+
secretKey: string2().max(300)
|
|
8172
|
+
});
|
|
8173
|
+
var StorageConnection = object({
|
|
8174
|
+
name: string2(),
|
|
8175
|
+
endpoint: string2(),
|
|
8176
|
+
region: string2(),
|
|
8177
|
+
verifiedAt: string2().nullable()
|
|
8178
|
+
});
|
|
8179
|
+
var CreateBucket = object({ connection: string2().max(40), binding: string2().max(31).optional() });
|
|
8180
|
+
var AppBucket = object({
|
|
8181
|
+
binding: string2(),
|
|
8182
|
+
bucket: string2(),
|
|
8183
|
+
connection: string2(),
|
|
8184
|
+
bytes: number2().nullable(),
|
|
8185
|
+
objects: number2().nullable(),
|
|
8186
|
+
measuredAt: string2().nullable()
|
|
8187
|
+
});
|
|
8188
|
+
var CreateEnvironment = object({
|
|
8189
|
+
name: string2().max(12),
|
|
8190
|
+
port: number2().int().optional(),
|
|
8191
|
+
branch: string2().max(100).optional()
|
|
8192
|
+
});
|
|
8193
|
+
var AppEnvironment = object({
|
|
8194
|
+
id: string2(),
|
|
8195
|
+
environment: string2(),
|
|
8196
|
+
port: number2().nullable(),
|
|
8197
|
+
deployedSha: string2().nullable()
|
|
8198
|
+
});
|
|
8199
|
+
var LicenceState = object({
|
|
8200
|
+
limited: boolean2(),
|
|
8201
|
+
plan: string2(),
|
|
8202
|
+
customer: string2().nullable(),
|
|
8203
|
+
maxApps: number2().nullable(),
|
|
8204
|
+
used: number2(),
|
|
8205
|
+
expires: string2().nullable(),
|
|
8206
|
+
problem: string2().nullable()
|
|
8207
|
+
});
|
|
8208
|
+
var RollbackPlanRequest = object({ deploymentId: string2().max(60) });
|
|
7782
8209
|
var DeployEvent = object({
|
|
7783
8210
|
id: number2().int(),
|
|
7784
8211
|
ts: string2(),
|
|
@@ -7809,10 +8236,13 @@ var EXIT = {
|
|
|
7809
8236
|
invalid: 2,
|
|
7810
8237
|
denied: 3,
|
|
7811
8238
|
connect: 4,
|
|
8239
|
+
confirm: 5,
|
|
8240
|
+
conflict: 6,
|
|
7812
8241
|
interrupted: 130
|
|
7813
8242
|
};
|
|
7814
8243
|
|
|
7815
8244
|
// src/runtime.ts
|
|
8245
|
+
import { spawn } from "node:child_process";
|
|
7816
8246
|
import fs from "node:fs";
|
|
7817
8247
|
import os from "node:os";
|
|
7818
8248
|
import path from "node:path";
|
|
@@ -7852,6 +8282,37 @@ function realRuntime() {
|
|
|
7852
8282
|
resolve(a);
|
|
7853
8283
|
});
|
|
7854
8284
|
}),
|
|
8285
|
+
color: Boolean(process.stdout.isTTY && !env.NO_COLOR && env.TERM !== "dumb") || Boolean(env.FORCE_COLOR),
|
|
8286
|
+
onEnter: (fn) => {
|
|
8287
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
8288
|
+
rl.once("line", () => {
|
|
8289
|
+
rl.close();
|
|
8290
|
+
fn();
|
|
8291
|
+
});
|
|
8292
|
+
return () => rl.close();
|
|
8293
|
+
},
|
|
8294
|
+
openUrl: (url) => {
|
|
8295
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
8296
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => {}).unref();
|
|
8297
|
+
},
|
|
8298
|
+
run: (cmd, args, opts = {}) => new Promise((resolve, reject) => {
|
|
8299
|
+
const child = spawn(cmd, args, {
|
|
8300
|
+
cwd: process.cwd(),
|
|
8301
|
+
env: { ...env, ...opts.env },
|
|
8302
|
+
stdio: opts.capture ? ["ignore", "pipe", "pipe"] : "inherit"
|
|
8303
|
+
});
|
|
8304
|
+
let out = "";
|
|
8305
|
+
child.stdout?.on("data", (d) => out += String(d));
|
|
8306
|
+
child.stderr?.on("data", (d) => out += String(d));
|
|
8307
|
+
child.once("error", reject);
|
|
8308
|
+
child.once("close", (code) => resolve({ code: code ?? 1, out }));
|
|
8309
|
+
}),
|
|
8310
|
+
readStdin: async () => {
|
|
8311
|
+
const chunks = [];
|
|
8312
|
+
for await (const c of process.stdin)
|
|
8313
|
+
chunks.push(c);
|
|
8314
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
8315
|
+
},
|
|
7855
8316
|
onInterrupt: (fn) => {
|
|
7856
8317
|
process.on("SIGINT", fn);
|
|
7857
8318
|
return () => process.off("SIGINT", fn);
|
|
@@ -7900,13 +8361,28 @@ function loadToken(rt, server) {
|
|
|
7900
8361
|
return null;
|
|
7901
8362
|
return c.token;
|
|
7902
8363
|
}
|
|
8364
|
+
var settingsFile = (rt) => path.join(rt.configDir, "settings.json");
|
|
8365
|
+
function savedServer(rt) {
|
|
8366
|
+
try {
|
|
8367
|
+
const s = JSON.parse(fs.readFileSync(settingsFile(rt), "utf8"));
|
|
8368
|
+
return s.server ? origin(s.server) : null;
|
|
8369
|
+
} catch {
|
|
8370
|
+
return null;
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
function saveServer(rt, server) {
|
|
8374
|
+
fs.mkdirSync(rt.configDir, { recursive: true, mode: 448 });
|
|
8375
|
+
fs.writeFileSync(settingsFile(rt), JSON.stringify({ server: origin(server) }, null, 2), {
|
|
8376
|
+
mode: 384
|
|
8377
|
+
});
|
|
8378
|
+
}
|
|
7903
8379
|
|
|
7904
8380
|
// src/client.ts
|
|
7905
8381
|
class ApiFailure extends CliError {
|
|
7906
8382
|
status;
|
|
7907
8383
|
code;
|
|
7908
8384
|
constructor(message, status, code) {
|
|
7909
|
-
super(message, status === 502 || status === 503 || status === 504 ? EXIT.connect : status === 401 || status === 403 || status === 404 ? EXIT.denied : status === 400 || status === 426 ? EXIT.invalid : EXIT.failed);
|
|
8385
|
+
super(message, status === 502 || status === 503 || status === 504 ? EXIT.connect : status === 401 || status === 403 || status === 404 ? EXIT.denied : status === 409 ? EXIT.conflict : status === 400 || status === 426 ? EXIT.invalid : EXIT.failed);
|
|
7910
8386
|
this.status = status;
|
|
7911
8387
|
this.code = code;
|
|
7912
8388
|
}
|
|
@@ -7970,20 +8446,270 @@ class Client {
|
|
|
7970
8446
|
}
|
|
7971
8447
|
}
|
|
7972
8448
|
|
|
7973
|
-
// src/
|
|
8449
|
+
// src/completion.ts
|
|
8450
|
+
var shown = (c) => c.commands.filter((s) => s.name() !== "help");
|
|
8451
|
+
var nextWords = (c) => [
|
|
8452
|
+
...shown(c).flatMap((s) => [s.name(), ...s.aliases()]),
|
|
8453
|
+
...c.options.flatMap((o) => o.long ? [o.long] : [])
|
|
8454
|
+
];
|
|
8455
|
+
function bash(program) {
|
|
8456
|
+
const cases = shown(program).map((c) => ` ${[c.name(), ...c.aliases()].join("|")}) words="${nextWords(c).join(" ")}" ;;`).join(`
|
|
8457
|
+
`);
|
|
8458
|
+
return `_orbito() {
|
|
8459
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}" words="${nextWords(program).join(" ")}"
|
|
8460
|
+
if [ "$COMP_CWORD" -gt 1 ]; then
|
|
8461
|
+
case "\${COMP_WORDS[1]}" in
|
|
8462
|
+
${cases}
|
|
8463
|
+
esac
|
|
8464
|
+
fi
|
|
8465
|
+
COMPREPLY=($(compgen -W "$words" -- "$cur"))
|
|
8466
|
+
}
|
|
8467
|
+
complete -F _orbito orbito
|
|
8468
|
+
`;
|
|
8469
|
+
}
|
|
8470
|
+
function fish(program) {
|
|
8471
|
+
const lines = [
|
|
8472
|
+
`complete -c orbito -f -n __fish_use_subcommand -a "${shown(program).flatMap((c) => [c.name(), ...c.aliases()]).join(" ")}"`
|
|
8473
|
+
];
|
|
8474
|
+
for (const c of shown(program)) {
|
|
8475
|
+
const names = [c.name(), ...c.aliases()].join(" ");
|
|
8476
|
+
const subs = shown(c).map((s) => s.name());
|
|
8477
|
+
if (subs.length)
|
|
8478
|
+
lines.push(`complete -c orbito -f -n "__fish_seen_subcommand_from ${names}" -a "${subs.join(" ")}"`);
|
|
8479
|
+
for (const o of c.options)
|
|
8480
|
+
if (o.long)
|
|
8481
|
+
lines.push(`complete -c orbito -n "__fish_seen_subcommand_from ${names}" -l ${o.long.slice(2)}`);
|
|
8482
|
+
}
|
|
8483
|
+
return lines.join(`
|
|
8484
|
+
`) + `
|
|
8485
|
+
`;
|
|
8486
|
+
}
|
|
8487
|
+
function completionScript(shell, program) {
|
|
8488
|
+
if (shell === "bash")
|
|
8489
|
+
return bash(program);
|
|
8490
|
+
if (shell === "zsh")
|
|
8491
|
+
return `autoload -U +X compinit && compinit
|
|
8492
|
+
autoload -U +X bashcompinit && bashcompinit
|
|
8493
|
+
${bash(program)}`;
|
|
8494
|
+
if (shell === "fish")
|
|
8495
|
+
return fish(program);
|
|
8496
|
+
return null;
|
|
8497
|
+
}
|
|
8498
|
+
|
|
8499
|
+
// src/dev.ts
|
|
8500
|
+
import crypto from "node:crypto";
|
|
7974
8501
|
import fs2 from "node:fs";
|
|
8502
|
+
import net from "node:net";
|
|
7975
8503
|
import path2 from "node:path";
|
|
8504
|
+
var ENV_FILE = ".env.local";
|
|
8505
|
+
var STATE_DIR = ".orbito";
|
|
8506
|
+
var DEFAULT_POSTGRES = "18.6";
|
|
8507
|
+
var SQLITE_FILE = "dev.db";
|
|
8508
|
+
var containerName = (c) => `orbito-dev-${c.appId}`;
|
|
8509
|
+
var stateFile = (rt) => path2.join(rt.cwd, STATE_DIR, "postgres.json");
|
|
8510
|
+
function packageManager(cwd) {
|
|
8511
|
+
if (["bun.lock", "bun.lockb"].some((f) => fs2.existsSync(path2.join(cwd, f))))
|
|
8512
|
+
return "bun";
|
|
8513
|
+
if (fs2.existsSync(path2.join(cwd, "pnpm-lock.yaml")))
|
|
8514
|
+
return "pnpm";
|
|
8515
|
+
return "npm";
|
|
8516
|
+
}
|
|
8517
|
+
function scriptsOf(cwd) {
|
|
8518
|
+
try {
|
|
8519
|
+
const pkg = JSON.parse(fs2.readFileSync(path2.join(cwd, "package.json"), "utf8"));
|
|
8520
|
+
return pkg.scripts ?? {};
|
|
8521
|
+
} catch {
|
|
8522
|
+
return {};
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
function parseEnv(text) {
|
|
8526
|
+
const out = {};
|
|
8527
|
+
for (const line of text.split(/\r?\n/)) {
|
|
8528
|
+
const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
8529
|
+
if (!m)
|
|
8530
|
+
continue;
|
|
8531
|
+
const v = m[2].trim();
|
|
8532
|
+
out[m[1]] = /^(['"]).*\1$/.test(v) ? v.slice(1, -1) : v;
|
|
8533
|
+
}
|
|
8534
|
+
return out;
|
|
8535
|
+
}
|
|
8536
|
+
var readEnvLocal = (rt) => {
|
|
8537
|
+
try {
|
|
8538
|
+
return parseEnv(fs2.readFileSync(path2.join(rt.cwd, ENV_FILE), "utf8"));
|
|
8539
|
+
} catch {
|
|
8540
|
+
return {};
|
|
8541
|
+
}
|
|
8542
|
+
};
|
|
8543
|
+
function writeEnvLocal(rt, values) {
|
|
8544
|
+
const file = path2.join(rt.cwd, ENV_FILE);
|
|
8545
|
+
const lines = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8").split(`
|
|
8546
|
+
`) : [];
|
|
8547
|
+
const left = new Map(Object.entries(values));
|
|
8548
|
+
const out = lines.map((line) => {
|
|
8549
|
+
const name = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=/.exec(line)?.[1];
|
|
8550
|
+
if (!name || !left.has(name))
|
|
8551
|
+
return line;
|
|
8552
|
+
const v = left.get(name);
|
|
8553
|
+
left.delete(name);
|
|
8554
|
+
return `${name}=${v}`;
|
|
8555
|
+
});
|
|
8556
|
+
while (out.length > 0 && out[out.length - 1] === "")
|
|
8557
|
+
out.pop();
|
|
8558
|
+
for (const [k, v] of left)
|
|
8559
|
+
out.push(`${k}=${v}`);
|
|
8560
|
+
fs2.writeFileSync(file, `${out.join(`
|
|
8561
|
+
`)}
|
|
8562
|
+
`, { mode: 384 });
|
|
8563
|
+
fs2.chmodSync(file, 384);
|
|
8564
|
+
}
|
|
8565
|
+
async function ensureIgnored(rt) {
|
|
8566
|
+
const tracked = await rt.run("git", ["ls-files", "--error-unmatch", ENV_FILE], { capture: true }).catch(() => null);
|
|
8567
|
+
if (tracked?.code === 0)
|
|
8568
|
+
throw new CliError(`${ENV_FILE} is committed to git. Remove it first: git rm --cached ${ENV_FILE}`, EXIT.failed);
|
|
8569
|
+
const file = path2.join(rt.cwd, ".gitignore");
|
|
8570
|
+
const text = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
|
|
8571
|
+
const have = text.split(`
|
|
8572
|
+
`).map((l) => l.trim());
|
|
8573
|
+
const missing = [ENV_FILE, `${STATE_DIR}/`].filter((p) => !have.includes(p));
|
|
8574
|
+
if (missing.length > 0)
|
|
8575
|
+
fs2.writeFileSync(file, `${text}${text && !text.endsWith(`
|
|
8576
|
+
`) ? `
|
|
8577
|
+
` : ""}${missing.join(`
|
|
8578
|
+
`)}
|
|
8579
|
+
`);
|
|
8580
|
+
}
|
|
8581
|
+
var isLocalUrl = (url) => {
|
|
8582
|
+
if (url.startsWith("file:"))
|
|
8583
|
+
return true;
|
|
8584
|
+
try {
|
|
8585
|
+
return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(url).hostname);
|
|
8586
|
+
} catch {
|
|
8587
|
+
return false;
|
|
8588
|
+
}
|
|
8589
|
+
};
|
|
8590
|
+
var freePort = () => new Promise((resolve, reject) => {
|
|
8591
|
+
const s = net.createServer();
|
|
8592
|
+
s.once("error", reject);
|
|
8593
|
+
s.listen(0, "127.0.0.1", () => {
|
|
8594
|
+
const { port } = s.address();
|
|
8595
|
+
s.close(() => resolve(port));
|
|
8596
|
+
});
|
|
8597
|
+
});
|
|
8598
|
+
async function docker(rt, args) {
|
|
8599
|
+
const r = await rt.run("docker", args, { capture: true }).catch(() => null);
|
|
8600
|
+
if (!r)
|
|
8601
|
+
throw new CliError("Docker is needed for a local PostgreSQL. Install and start it, or use SQLite.", EXIT.failed);
|
|
8602
|
+
return r;
|
|
8603
|
+
}
|
|
8604
|
+
async function startPostgres(rt, c, version) {
|
|
8605
|
+
const name = containerName(c);
|
|
8606
|
+
let state = null;
|
|
8607
|
+
try {
|
|
8608
|
+
state = JSON.parse(fs2.readFileSync(stateFile(rt), "utf8"));
|
|
8609
|
+
} catch {}
|
|
8610
|
+
const running = await docker(rt, ["inspect", "-f", "{{.State.Running}}", name]);
|
|
8611
|
+
if (running.code !== 0) {
|
|
8612
|
+
state ??= { password: crypto.randomBytes(18).toString("base64url"), port: await freePort() };
|
|
8613
|
+
const data = Number(version.split(".")[0]) >= 18 ? "/var/lib/postgresql" : "/var/lib/postgresql/data";
|
|
8614
|
+
const made = await docker(rt, [
|
|
8615
|
+
"run",
|
|
8616
|
+
"-d",
|
|
8617
|
+
"--name",
|
|
8618
|
+
name,
|
|
8619
|
+
"-e",
|
|
8620
|
+
`POSTGRES_PASSWORD=${state.password}`,
|
|
8621
|
+
"-e",
|
|
8622
|
+
"POSTGRES_DB=app",
|
|
8623
|
+
"-p",
|
|
8624
|
+
`127.0.0.1:${state.port}:5432`,
|
|
8625
|
+
"-v",
|
|
8626
|
+
`${name}:${data}`,
|
|
8627
|
+
`postgres:${version}-alpine`
|
|
8628
|
+
]);
|
|
8629
|
+
if (made.code !== 0)
|
|
8630
|
+
throw new CliError(`Could not start PostgreSQL: ${made.out}`, EXIT.failed);
|
|
8631
|
+
fs2.mkdirSync(path2.dirname(stateFile(rt)), { recursive: true });
|
|
8632
|
+
fs2.writeFileSync(stateFile(rt), JSON.stringify(state), { mode: 384 });
|
|
8633
|
+
} else if (running.out.trim() !== "true") {
|
|
8634
|
+
await docker(rt, ["start", name]);
|
|
8635
|
+
}
|
|
8636
|
+
if (!state)
|
|
8637
|
+
throw new CliError(`The container ${name} exists but ${stateFile(rt)} is gone. Run: orbito db reset`, EXIT.failed);
|
|
8638
|
+
for (let i = 0;i < 90; i++) {
|
|
8639
|
+
const ready = await docker(rt, ["exec", name, "pg_isready", "-U", "postgres", "-d", "app", "-h", "127.0.0.1"]);
|
|
8640
|
+
if (ready.code === 0)
|
|
8641
|
+
return `postgresql://postgres:${encodeURIComponent(state.password)}@127.0.0.1:${state.port}/app`;
|
|
8642
|
+
await rt.sleep(1000);
|
|
8643
|
+
}
|
|
8644
|
+
throw new CliError("PostgreSQL did not become ready in time.", EXIT.failed);
|
|
8645
|
+
}
|
|
8646
|
+
async function setupDatabase(rt, c) {
|
|
8647
|
+
await ensureIgnored(rt);
|
|
8648
|
+
const existing = readEnvLocal(rt).DATABASE_URL;
|
|
8649
|
+
const engine = c.dev?.database;
|
|
8650
|
+
const managedUrl = existing?.startsWith(`file:./${STATE_DIR}/`);
|
|
8651
|
+
if (existing && !isLocalUrl(existing))
|
|
8652
|
+
throw new CliError("DATABASE_URL in .env.local is not on this machine. orbito dev never connects to a remote database. Remove it, then run this again.", EXIT.failed);
|
|
8653
|
+
if (existing && !managedUrl && !fs2.existsSync(stateFile(rt)))
|
|
8654
|
+
return { engine: "external", url: existing };
|
|
8655
|
+
if (engine === "postgres") {
|
|
8656
|
+
const url = await startPostgres(rt, c, c.dev?.postgres ?? DEFAULT_POSTGRES);
|
|
8657
|
+
writeEnvLocal(rt, { DATABASE_URL: url });
|
|
8658
|
+
return { engine: "postgres", url };
|
|
8659
|
+
}
|
|
8660
|
+
if (engine === "sqlite") {
|
|
8661
|
+
fs2.mkdirSync(path2.join(rt.cwd, STATE_DIR), { recursive: true });
|
|
8662
|
+
const url = `file:./${STATE_DIR}/${SQLITE_FILE}`;
|
|
8663
|
+
writeEnvLocal(rt, { DATABASE_URL: url });
|
|
8664
|
+
return { engine: "sqlite", url };
|
|
8665
|
+
}
|
|
8666
|
+
return { engine: "none", url: null };
|
|
8667
|
+
}
|
|
8668
|
+
async function runScript(rt, script) {
|
|
8669
|
+
if (!scriptsOf(rt.cwd)[script])
|
|
8670
|
+
return null;
|
|
8671
|
+
const pm = packageManager(rt.cwd);
|
|
8672
|
+
const r = await rt.run(pm, ["run", script], { env: readEnvLocal(rt) });
|
|
8673
|
+
return r.code;
|
|
8674
|
+
}
|
|
8675
|
+
async function dropDatabase(rt, c) {
|
|
8676
|
+
if (c.dev?.database === "postgres") {
|
|
8677
|
+
const name = containerName(c);
|
|
8678
|
+
await docker(rt, ["rm", "-f", name]);
|
|
8679
|
+
await docker(rt, ["volume", "rm", "-f", name]);
|
|
8680
|
+
fs2.rmSync(stateFile(rt), { force: true });
|
|
8681
|
+
const file = path2.join(rt.cwd, ENV_FILE);
|
|
8682
|
+
if (fs2.existsSync(file))
|
|
8683
|
+
fs2.writeFileSync(file, fs2.readFileSync(file, "utf8").replace(/^\s*(?:export\s+)?DATABASE_URL=.*\n?/m, ""));
|
|
8684
|
+
} else if (c.dev?.database === "sqlite") {
|
|
8685
|
+
for (const suffix of ["", "-wal", "-shm"])
|
|
8686
|
+
fs2.rmSync(path2.join(rt.cwd, STATE_DIR, `${SQLITE_FILE}${suffix}`), { force: true });
|
|
8687
|
+
} else {
|
|
8688
|
+
throw new CliError('Say which local database this app uses: "dev": { "database": "sqlite" } in orbito.json.', EXIT.invalid);
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
function shellCommand(c, cwd) {
|
|
8692
|
+
if (c.dev?.database === "postgres")
|
|
8693
|
+
return ["docker", ["exec", "-it", containerName(c), "psql", "-U", "postgres", "-d", "app"]];
|
|
8694
|
+
if (c.dev?.database === "sqlite")
|
|
8695
|
+
return ["sqlite3", [path2.join(cwd, STATE_DIR, SQLITE_FILE)]];
|
|
8696
|
+
throw new CliError('Say which local database this app uses: "dev": { "database": "sqlite" } in orbito.json.', EXIT.invalid);
|
|
8697
|
+
}
|
|
8698
|
+
|
|
8699
|
+
// src/config.ts
|
|
8700
|
+
import fs3 from "node:fs";
|
|
8701
|
+
import path3 from "node:path";
|
|
7976
8702
|
var CONFIG_FILE = "orbito.json";
|
|
7977
8703
|
function configPath(rt, explicit) {
|
|
7978
|
-
return
|
|
8704
|
+
return path3.resolve(rt.cwd, explicit ?? CONFIG_FILE);
|
|
7979
8705
|
}
|
|
7980
8706
|
function loadConfig(rt, explicit) {
|
|
7981
8707
|
const file = configPath(rt, explicit);
|
|
7982
8708
|
let raw;
|
|
7983
8709
|
try {
|
|
7984
|
-
raw =
|
|
8710
|
+
raw = fs3.readFileSync(file, "utf8");
|
|
7985
8711
|
} catch {
|
|
7986
|
-
throw new CliError(`No ${
|
|
8712
|
+
throw new CliError(`No ${path3.basename(file)} found at ${file}. Run: orbito init --app <id>`, EXIT.invalid);
|
|
7987
8713
|
}
|
|
7988
8714
|
let json;
|
|
7989
8715
|
try {
|
|
@@ -8002,11 +8728,28 @@ ${lines.join(`
|
|
|
8002
8728
|
}
|
|
8003
8729
|
function writeConfig(rt, config, explicit) {
|
|
8004
8730
|
const file = configPath(rt, explicit);
|
|
8005
|
-
|
|
8731
|
+
fs3.writeFileSync(file, JSON.stringify(config, null, 2) + `
|
|
8006
8732
|
`);
|
|
8007
8733
|
return file;
|
|
8008
8734
|
}
|
|
8009
8735
|
|
|
8736
|
+
// src/ui.ts
|
|
8737
|
+
function styles(rt) {
|
|
8738
|
+
const on = rt.color === true;
|
|
8739
|
+
const wrap = (open, close) => (t) => on ? `\x1B[${open}m${t}\x1B[${close}m` : t;
|
|
8740
|
+
return {
|
|
8741
|
+
orange: wrap("38;5;208", "39"),
|
|
8742
|
+
bold: wrap("1", "22"),
|
|
8743
|
+
dim: wrap("2", "22"),
|
|
8744
|
+
green: wrap("32", "39")
|
|
8745
|
+
};
|
|
8746
|
+
}
|
|
8747
|
+
function table(rows, gap = 2) {
|
|
8748
|
+
const len = (t) => t.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
8749
|
+
const widths = rows[0].map((_, i) => Math.max(...rows.map((r) => len(r[i] ?? ""))));
|
|
8750
|
+
return rows.map((r) => r.map((c, i) => i === r.length - 1 ? c : c + " ".repeat(widths[i] - len(c) + gap)).join(""));
|
|
8751
|
+
}
|
|
8752
|
+
|
|
8010
8753
|
// src/cli.ts
|
|
8011
8754
|
var VERSION = package_default.version;
|
|
8012
8755
|
var DEPLOY_SCRIPTS = {
|
|
@@ -8018,7 +8761,24 @@ var DEPLOY_SCRIPTS = {
|
|
|
8018
8761
|
async function run(argv, rt) {
|
|
8019
8762
|
const program = new Command2;
|
|
8020
8763
|
let exit = EXIT.ok;
|
|
8021
|
-
program.name("orbito").version(VERSION).option("--config <path>", "path to orbito.json (for monorepos)").option("--json", "machine-readable output on stdout (newline-delimited for streams)").exitOverride().configureOutput({ writeOut: rt.out, writeErr: rt.err })
|
|
8764
|
+
program.name("orbito").version(`${VERSION} (CLI API ${API_VERSION})`).option("--config <path>", "path to orbito.json (for monorepos)").option("--json", "machine-readable output on stdout (newline-delimited for streams)").option("--env <name>", "act on another environment of the app, for example staging").exitOverride().configureOutput({ writeOut: rt.out, writeErr: rt.err }).addHelpText("after", `
|
|
8765
|
+
Examples:
|
|
8766
|
+
orbito login
|
|
8767
|
+
orbito whoami
|
|
8768
|
+
orbito deploy --dry-run
|
|
8769
|
+
orbito deploy --yes --json
|
|
8770
|
+
orbito logs --lines 50
|
|
8771
|
+
|
|
8772
|
+
Exit codes:
|
|
8773
|
+
0 success
|
|
8774
|
+
1 the operation failed
|
|
8775
|
+
2 invalid usage or configuration
|
|
8776
|
+
3 not logged in, or no access
|
|
8777
|
+
4 server unreachable
|
|
8778
|
+
5 needs confirmation (no terminal and no --yes, or answered no)
|
|
8779
|
+
6 refused by the server because of the current state
|
|
8780
|
+
130 stopped following with Ctrl+C (the job continues on the server)
|
|
8781
|
+
`);
|
|
8022
8782
|
const g = () => program.opts();
|
|
8023
8783
|
const emit = (v) => rt.out(JSON.stringify(v) + `
|
|
8024
8784
|
`);
|
|
@@ -8026,11 +8786,32 @@ async function run(argv, rt) {
|
|
|
8026
8786
|
`);
|
|
8027
8787
|
const say = (s) => g().json ? info(s) : rt.out(s + `
|
|
8028
8788
|
`);
|
|
8029
|
-
const
|
|
8789
|
+
const confirmOr = async (question, yes) => {
|
|
8790
|
+
if (yes)
|
|
8791
|
+
return;
|
|
8792
|
+
if (!rt.isTTY)
|
|
8793
|
+
throw new CliError("Not a terminal: pass --yes to accept the plan.", EXIT.confirm);
|
|
8794
|
+
if (!await rt.confirm(question))
|
|
8795
|
+
throw new CliError("Cancelled.", EXIT.confirm);
|
|
8796
|
+
};
|
|
8797
|
+
const cfg = () => {
|
|
8798
|
+
const c = loadConfig(rt, g().config);
|
|
8799
|
+
const app = rt.env.ORBITO_APP;
|
|
8800
|
+
if (app && !APP_ID.test(app))
|
|
8801
|
+
throw new CliError(`ORBITO_APP is not an app id: ${app}`, EXIT.invalid);
|
|
8802
|
+
const base = app || c.appId;
|
|
8803
|
+
const env = g().env;
|
|
8804
|
+
if (!env || env === "production")
|
|
8805
|
+
return { ...c, appId: base };
|
|
8806
|
+
const appId = `${base}-${env}`;
|
|
8807
|
+
if (!APP_ID.test(appId))
|
|
8808
|
+
throw new CliError(`'${env}' is not an environment name.`, EXIT.invalid);
|
|
8809
|
+
return { ...c, appId };
|
|
8810
|
+
};
|
|
8030
8811
|
const clientFor = (server, needToken = true) => {
|
|
8031
8812
|
const token = loadToken(rt, server);
|
|
8032
8813
|
if (needToken && !token)
|
|
8033
|
-
throw new CliError(`Not logged in to ${server}. Run: orbito login
|
|
8814
|
+
throw new CliError(`Not logged in to ${server}. Run: orbito login`, EXIT.denied);
|
|
8034
8815
|
return new Client(rt, server, token);
|
|
8035
8816
|
};
|
|
8036
8817
|
const authed = () => {
|
|
@@ -8039,10 +8820,35 @@ async function run(argv, rt) {
|
|
|
8039
8820
|
};
|
|
8040
8821
|
async function checkServer(api) {
|
|
8041
8822
|
const caps = Capabilities.safeParse(await api.call("GET", "/capabilities"));
|
|
8042
|
-
if (!caps.success
|
|
8823
|
+
if (!caps.success)
|
|
8043
8824
|
throw new CliError(`This server does not speak CLI API ${API_VERSION}. Update the CLI or the server.`, EXIT.invalid);
|
|
8825
|
+
if (caps.data.api > API_VERSION)
|
|
8826
|
+
throw new CliError(`This server speaks CLI API ${caps.data.api}, this CLI speaks ${API_VERSION}. Update the CLI: npm install -g @deaduchiha/orbito`, EXIT.invalid);
|
|
8827
|
+
if (caps.data.api < API_VERSION)
|
|
8828
|
+
throw new CliError(`This server speaks CLI API ${caps.data.api}, this CLI speaks ${API_VERSION}. Update the server: orbito-update`, EXIT.invalid);
|
|
8044
8829
|
return caps.data;
|
|
8045
8830
|
}
|
|
8831
|
+
async function resolveServer(flag) {
|
|
8832
|
+
let raw = flag || rt.env.ORBITO_SERVER;
|
|
8833
|
+
if (!raw) {
|
|
8834
|
+
try {
|
|
8835
|
+
raw = loadConfig(rt, g().config).server;
|
|
8836
|
+
} catch {}
|
|
8837
|
+
}
|
|
8838
|
+
raw ||= savedServer(rt) ?? undefined;
|
|
8839
|
+
if (!raw) {
|
|
8840
|
+
if (!rt.isTTY)
|
|
8841
|
+
throw new CliError("No server. Set ORBITO_SERVER or pass --server <url>.", EXIT.invalid);
|
|
8842
|
+
raw = (await rt.ask("Orbito server (for example orbito.example.com): ")).trim();
|
|
8843
|
+
if (!raw)
|
|
8844
|
+
throw new CliError("Cancelled.", EXIT.invalid);
|
|
8845
|
+
}
|
|
8846
|
+
try {
|
|
8847
|
+
return new URL(/^https?:\/\//.test(raw) ? raw : `https://${raw}`).origin;
|
|
8848
|
+
} catch {
|
|
8849
|
+
throw new CliError("The server must be a URL, for example https://orbito.example.com", EXIT.invalid);
|
|
8850
|
+
}
|
|
8851
|
+
}
|
|
8046
8852
|
async function loginTo(serverUrl) {
|
|
8047
8853
|
let server;
|
|
8048
8854
|
try {
|
|
@@ -8072,47 +8878,60 @@ async function run(argv, rt) {
|
|
|
8072
8878
|
if (!start.ok)
|
|
8073
8879
|
throw new CliError(`The server refused the login (${start.status}).`, EXIT.denied);
|
|
8074
8880
|
const d = await start.json();
|
|
8075
|
-
|
|
8881
|
+
const link = d.verification_uri_complete ?? d.verification_uri;
|
|
8882
|
+
info(`Open ${link} and check that the code is ${d.user_code}.`);
|
|
8076
8883
|
info("If you are asked to sign in, sign in and open the link again.");
|
|
8077
|
-
let
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
8083
|
-
device_code: d.device_code,
|
|
8084
|
-
client_id: CLI_CLIENT_ID
|
|
8085
|
-
});
|
|
8086
|
-
const body = await t.json().catch(() => ({}));
|
|
8087
|
-
if (t.ok && body.access_token) {
|
|
8088
|
-
const ex = await new Client(rt, server.origin, null).call("POST", "/auth/exchange", {
|
|
8089
|
-
sessionToken: body.access_token,
|
|
8090
|
-
label: `orbito cli on ${rt.hostname}`
|
|
8091
|
-
});
|
|
8092
|
-
saveCredential(rt, server.origin, {
|
|
8093
|
-
token: ex.token,
|
|
8094
|
-
expiresAt: ex.expiresAt
|
|
8095
|
-
});
|
|
8096
|
-
say(`Logged in to ${server.origin}. Apps: ${ex.apps.map((a) => `${a.id} (${a.level})`).join(", ") || "none yet"}`);
|
|
8097
|
-
if (g().json)
|
|
8098
|
-
emit({ type: "login", server: server.origin, apps: ex.apps });
|
|
8099
|
-
return;
|
|
8884
|
+
let stopEnter = () => {};
|
|
8885
|
+
try {
|
|
8886
|
+
if (rt.isTTY && rt.onEnter && rt.openUrl && new URL(link).origin === server.origin) {
|
|
8887
|
+
info("Press Enter to open it in your browser.");
|
|
8888
|
+
stopEnter = rt.onEnter(() => rt.openUrl(link));
|
|
8100
8889
|
}
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8890
|
+
} catch {}
|
|
8891
|
+
try {
|
|
8892
|
+
let wait = Math.max(1, d.interval) * 1000;
|
|
8893
|
+
const until = Date.now() + d.expires_in * 1000;
|
|
8894
|
+
while (Date.now() < until) {
|
|
8895
|
+
await rt.sleep(wait);
|
|
8896
|
+
const t = await post("/device/token", {
|
|
8897
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
8898
|
+
device_code: d.device_code,
|
|
8899
|
+
client_id: CLI_CLIENT_ID
|
|
8900
|
+
});
|
|
8901
|
+
const body = await t.json().catch(() => ({}));
|
|
8902
|
+
if (t.ok && body.access_token) {
|
|
8903
|
+
const ex = await new Client(rt, server.origin, null).call("POST", "/auth/exchange", {
|
|
8904
|
+
sessionToken: body.access_token,
|
|
8905
|
+
label: `orbito cli on ${rt.hostname}`
|
|
8906
|
+
});
|
|
8907
|
+
saveServer(rt, server.origin);
|
|
8908
|
+
saveCredential(rt, server.origin, {
|
|
8909
|
+
token: ex.token,
|
|
8910
|
+
expiresAt: ex.expiresAt
|
|
8911
|
+
});
|
|
8912
|
+
say(`Logged in to ${server.origin}. Apps: ${ex.apps.map((a) => `${a.id} (${a.level})`).join(", ") || "none yet"}`);
|
|
8913
|
+
if (g().json)
|
|
8914
|
+
emit({ type: "login", server: server.origin, apps: ex.apps });
|
|
8915
|
+
return;
|
|
8916
|
+
}
|
|
8917
|
+
if (body.error === "authorization_pending")
|
|
8918
|
+
continue;
|
|
8919
|
+
if (body.error === "slow_down" || t.status === 429) {
|
|
8920
|
+
wait += 5000;
|
|
8921
|
+
continue;
|
|
8922
|
+
}
|
|
8923
|
+
if (body.error === "access_denied")
|
|
8924
|
+
throw new CliError("The login was denied in the browser.", EXIT.denied);
|
|
8925
|
+
if (body.error === "expired_token")
|
|
8926
|
+
break;
|
|
8927
|
+
throw new CliError(`Login failed: ${body.error ?? t.status}`, EXIT.failed);
|
|
8106
8928
|
}
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
break;
|
|
8111
|
-
throw new CliError(`Login failed: ${body.error ?? t.status}`, EXIT.failed);
|
|
8929
|
+
throw new CliError("The login code expired. Run orbito login again.", EXIT.denied);
|
|
8930
|
+
} finally {
|
|
8931
|
+
stopEnter();
|
|
8112
8932
|
}
|
|
8113
|
-
throw new CliError("The login code expired. Run orbito login again.", EXIT.denied);
|
|
8114
8933
|
}
|
|
8115
|
-
program.command("login").description("authorize this machine through your browser").
|
|
8934
|
+
program.command("login").description("authorize this machine through your browser").option("--server <url>", "Orbito server; defaults to orbito.json, then the last server used, then asks").action(async (o) => loginTo(await resolveServer(o.server)));
|
|
8116
8935
|
program.command("logout").description("revoke and remove the machine credential").option("--server <url>", "defaults to the server in orbito.json").action(async (o) => {
|
|
8117
8936
|
const server = o.server ?? cfg().server;
|
|
8118
8937
|
const token = loadToken(rt, server);
|
|
@@ -8127,37 +8946,73 @@ async function run(argv, rt) {
|
|
|
8127
8946
|
forgetCredential(rt, server);
|
|
8128
8947
|
} catch {}
|
|
8129
8948
|
say("Logged out.");
|
|
8949
|
+
if (g().json)
|
|
8950
|
+
emit({ type: "logout", server });
|
|
8130
8951
|
});
|
|
8131
|
-
program.command("whoami").description("show server,
|
|
8132
|
-
|
|
8952
|
+
program.command("whoami").description("show who you are, the server, and your access").option("--all", "list every app you can reach").action(async (o) => {
|
|
8953
|
+
let c;
|
|
8954
|
+
let api;
|
|
8955
|
+
try {
|
|
8956
|
+
({ c, api } = authed());
|
|
8957
|
+
} catch (e) {
|
|
8958
|
+
if (!rt.env.ORBITO_SERVER && !savedServer(rt))
|
|
8959
|
+
throw e;
|
|
8960
|
+
c = { server: await resolveServer() };
|
|
8961
|
+
api = clientFor(c.server);
|
|
8962
|
+
}
|
|
8133
8963
|
const me = await api.call("GET", "/me");
|
|
8134
8964
|
if (g().json)
|
|
8135
8965
|
return emit({ server: c.server, ...me });
|
|
8136
|
-
|
|
8137
|
-
|
|
8966
|
+
const st = styles(rt);
|
|
8967
|
+
const host = new URL(c.server).host;
|
|
8968
|
+
const here = me.apps.find((a) => a.id === c.appId);
|
|
8969
|
+
const levels = new Map;
|
|
8138
8970
|
for (const a of me.apps)
|
|
8139
|
-
|
|
8971
|
+
levels.set(a.level, (levels.get(a.level) ?? 0) + 1);
|
|
8972
|
+
const summary = [...levels].map(([l, n]) => `${n} ${l}`).join(", ");
|
|
8973
|
+
const rows = [
|
|
8974
|
+
["Account", `${st.bold(me.identity.label)} ${st.dim(`(${me.identity.kind})`)}`],
|
|
8975
|
+
["Server", host],
|
|
8976
|
+
...c.appId ? [
|
|
8977
|
+
[
|
|
8978
|
+
"Project",
|
|
8979
|
+
here ? `${st.orange(here.id)} ${st.dim("·")} ${here.level}` : `${c.appId} ${st.dim("· no access")}`
|
|
8980
|
+
]
|
|
8981
|
+
] : [],
|
|
8982
|
+
[
|
|
8983
|
+
"Access",
|
|
8984
|
+
`${me.apps.length} app${me.apps.length === 1 ? "" : "s"} ${st.dim(summary ? `· ${summary}` : "")}`
|
|
8985
|
+
]
|
|
8986
|
+
];
|
|
8987
|
+
rt.out(`
|
|
8988
|
+
${st.orange("◆")} ${st.bold("Orbito")}
|
|
8989
|
+
|
|
8990
|
+
`);
|
|
8991
|
+
for (const l of table(rows.map(([k, v]) => [st.dim(k), v])))
|
|
8992
|
+
rt.out(` ${l}
|
|
8993
|
+
`);
|
|
8994
|
+
if (o.all) {
|
|
8995
|
+
rt.out(`
|
|
8996
|
+
`);
|
|
8997
|
+
const head = ["APP", "ACCESS", "NAME"].map((h) => st.dim(h));
|
|
8998
|
+
const body = me.apps.map((a) => [
|
|
8999
|
+
a.id === c.appId ? st.orange(`● ${a.id}`) : ` ${a.id}`,
|
|
9000
|
+
a.level,
|
|
9001
|
+
a.name
|
|
9002
|
+
]);
|
|
9003
|
+
for (const l of table([head.map((h, i) => i === 0 ? ` ${h}` : h), ...body]))
|
|
9004
|
+
rt.out(` ${l}
|
|
9005
|
+
`);
|
|
9006
|
+
} else if (me.apps.length > 1) {
|
|
9007
|
+
rt.out(`
|
|
9008
|
+
${st.dim("Run")} orbito whoami --all ${st.dim("to list every app.")}
|
|
9009
|
+
`);
|
|
9010
|
+
}
|
|
9011
|
+
rt.out(`
|
|
8140
9012
|
`);
|
|
8141
9013
|
});
|
|
8142
9014
|
program.command("init").description("link this repository to an Orbito app (asks what it needs)").option("--app <id>", "the app id; omit to pick from the apps you can reach").option("--server <url>", "Orbito server (defaults to the one in an existing orbito.json)").option("--scripts", "add deploy, tail, deploy:status and db:remote:status to package.json").action(async (o) => {
|
|
8143
|
-
|
|
8144
|
-
if (!server) {
|
|
8145
|
-
try {
|
|
8146
|
-
server = loadConfig(rt, g().config).server;
|
|
8147
|
-
} catch {
|
|
8148
|
-
if (!rt.isTTY)
|
|
8149
|
-
throw new CliError("Pass --server <url> the first time.", EXIT.invalid);
|
|
8150
|
-
const a = (await rt.ask("Orbito server (for example orbito.example.com): ")).trim();
|
|
8151
|
-
if (!a)
|
|
8152
|
-
throw new CliError("Cancelled.", EXIT.invalid);
|
|
8153
|
-
server = /^https?:\/\//.test(a) ? a : `https://${a}`;
|
|
8154
|
-
}
|
|
8155
|
-
}
|
|
8156
|
-
try {
|
|
8157
|
-
server = new URL(server).origin;
|
|
8158
|
-
} catch {
|
|
8159
|
-
throw new CliError("The server must be a URL, for example https://orbito.example.com", EXIT.invalid);
|
|
8160
|
-
}
|
|
9015
|
+
const server = await resolveServer(o.server);
|
|
8161
9016
|
if (!loadToken(rt, server)) {
|
|
8162
9017
|
info(`Not logged in to ${server} yet, logging in first.`);
|
|
8163
9018
|
await loginTo(server);
|
|
@@ -8193,9 +9048,9 @@ ${list}`);
|
|
|
8193
9048
|
appId: app
|
|
8194
9049
|
}, g().config);
|
|
8195
9050
|
say(`Linked to '${app}' on ${server} (you have '${mine.level}'). Wrote ${file}; commit it, credentials stay outside the repository.`);
|
|
8196
|
-
const pkgFile =
|
|
8197
|
-
if (
|
|
8198
|
-
const pkg = JSON.parse(
|
|
9051
|
+
const pkgFile = path4.join(path4.dirname(file), "package.json");
|
|
9052
|
+
if (fs4.existsSync(pkgFile)) {
|
|
9053
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgFile, "utf8"));
|
|
8199
9054
|
const add = Object.entries(DEPLOY_SCRIPTS).filter(([k]) => !pkg.scripts?.[k]);
|
|
8200
9055
|
if (add.length > 0) {
|
|
8201
9056
|
say(`package.json scripts to add:
|
|
@@ -8204,7 +9059,7 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8204
9059
|
const yes = o.scripts || rt.isTTY && !/^n/i.test((await rt.ask("Add them? [Y/n] ")).trim());
|
|
8205
9060
|
if (yes) {
|
|
8206
9061
|
pkg.scripts = { ...pkg.scripts, ...Object.fromEntries(add) };
|
|
8207
|
-
|
|
9062
|
+
fs4.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + `
|
|
8208
9063
|
`);
|
|
8209
9064
|
say("Added.");
|
|
8210
9065
|
} else
|
|
@@ -8213,6 +9068,8 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8213
9068
|
}
|
|
8214
9069
|
say(`
|
|
8215
9070
|
Next: orbito doctor then orbito deploy --dry-run`);
|
|
9071
|
+
if (g().json)
|
|
9072
|
+
emit({ type: "init", server, appId: app, level: mine.level, file });
|
|
8216
9073
|
});
|
|
8217
9074
|
const configCmd = program.command("config").description("configuration commands");
|
|
8218
9075
|
configCmd.command("validate").description("validate orbito.json and server compatibility").action(async () => {
|
|
@@ -8222,6 +9079,34 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8222
9079
|
if (g().json)
|
|
8223
9080
|
emit({ ok: true, server: c.server, appId: c.appId, api: caps.api });
|
|
8224
9081
|
});
|
|
9082
|
+
configCmd.command("show").description("show each setting and where it came from (never prints a token)").action(() => {
|
|
9083
|
+
const c = cfg();
|
|
9084
|
+
const file = configPath(rt, g().config);
|
|
9085
|
+
const settings = {
|
|
9086
|
+
server: { value: c.server, source: file },
|
|
9087
|
+
appId: { value: c.appId, source: rt.env.ORBITO_APP ? "ORBITO_APP" : file },
|
|
9088
|
+
token: {
|
|
9089
|
+
source: rt.env.ORBITO_TOKEN ? "ORBITO_TOKEN" : loadToken(rt, c.server) ? "saved login" : "none"
|
|
9090
|
+
}
|
|
9091
|
+
};
|
|
9092
|
+
if (g().json)
|
|
9093
|
+
return emit({ type: "config", ...settings });
|
|
9094
|
+
rt.out(`server ${c.server} (${settings.server.source})
|
|
9095
|
+
`);
|
|
9096
|
+
rt.out(`appId ${c.appId} (${settings.appId.source})
|
|
9097
|
+
`);
|
|
9098
|
+
rt.out(`token ${settings.token.source}
|
|
9099
|
+
`);
|
|
9100
|
+
});
|
|
9101
|
+
program.command("completion").description("print a shell completion script").argument("<shell>", "bash, zsh or fish").addHelpText("after", `
|
|
9102
|
+
Example:
|
|
9103
|
+
orbito completion zsh >> ~/.zshrc
|
|
9104
|
+
`).action((shell) => {
|
|
9105
|
+
const script = completionScript(shell, program);
|
|
9106
|
+
if (!script)
|
|
9107
|
+
throw new CliError("Supported shells: bash, zsh, fish.", EXIT.invalid);
|
|
9108
|
+
rt.out(script);
|
|
9109
|
+
});
|
|
8225
9110
|
program.command("status").description("deployed commit, health and active job").action(async () => {
|
|
8226
9111
|
const { c, api } = authed();
|
|
8227
9112
|
const s = await api.call("GET", `/apps/${c.appId}/status`, undefined, AppStatus);
|
|
@@ -8280,12 +9165,14 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8280
9165
|
if (g().json)
|
|
8281
9166
|
emit({ type: "result", ...d });
|
|
8282
9167
|
else
|
|
8283
|
-
say(d.status === "deployed" ? `Done: ${d.sha?.slice(0, 12) ?? ""} is live
|
|
9168
|
+
say(d.status === "deployed" ? `Done: ${d.sha?.slice(0, 12) ?? ""} is live.${d.reason ? ` ${d.reason}` : ""}` : `${d.status}: ${d.reason ?? ""}`);
|
|
8284
9169
|
return d.status === "deployed" ? EXIT.ok : EXIT.failed;
|
|
8285
9170
|
}
|
|
8286
|
-
program.command("deploy").description("plan, confirm, deploy and follow progress").option("--dry-run", "show the target commit, recipe, migrations and blockers; change nothing").option("--detach", "submit and return the deployment id").option("--yes", "accept the displayed plan without asking (never skips permissions or blockers)").action(async (o) => {
|
|
9171
|
+
program.command("deploy").description("plan, confirm, deploy and follow progress").option("--dry-run", "show the target commit, recipe, migrations and blockers; change nothing").option("--detach", "submit and return the deployment id").option("--yes", "accept the displayed plan without asking (never skips permissions or blockers)").option("--env-only", "apply the variables only: keep the running commit, run no migrations").action(async (o) => {
|
|
8287
9172
|
const { c, api } = authed();
|
|
8288
9173
|
const caps = await checkServer(api);
|
|
9174
|
+
if (o.envOnly && !caps.features.envOnly)
|
|
9175
|
+
throw new CliError("This server cannot apply variables only. Update the server: orbito-update", EXIT.invalid);
|
|
8289
9176
|
const plan = await api.call("POST", `/apps/${c.appId}/deploy/plan`, {}, DeployPlan);
|
|
8290
9177
|
if (g().json)
|
|
8291
9178
|
emit({ type: "plan", ...plan });
|
|
@@ -8297,7 +9184,10 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8297
9184
|
if (plan.recipe)
|
|
8298
9185
|
rt.out(` recipe install ${plan.recipe.install}, build ${plan.recipe.build}, migrate ${plan.recipe.migrate}, restart ${plan.recipe.restart}
|
|
8299
9186
|
`);
|
|
8300
|
-
rt.out(` migration ${plan.migration.required ? `yes (needs '${plan.migration.neededLevel}')` : "no"}
|
|
9187
|
+
rt.out(` migration ${plan.migration.required && !o.envOnly ? `yes (needs '${plan.migration.neededLevel}')` : "no"}
|
|
9188
|
+
`);
|
|
9189
|
+
if (o.envOnly)
|
|
9190
|
+
rt.out(` NOTE variables only: the running commit stays.
|
|
8301
9191
|
`);
|
|
8302
9192
|
for (const b of plan.blockers)
|
|
8303
9193
|
rt.out(` BLOCKED ${b}
|
|
@@ -8314,19 +9204,15 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8314
9204
|
return;
|
|
8315
9205
|
if (!plan.sha)
|
|
8316
9206
|
throw new CliError("The commit could not be resolved.", EXIT.failed);
|
|
8317
|
-
if (plan.upToDate)
|
|
9207
|
+
if (plan.upToDate && !o.envOnly)
|
|
8318
9208
|
return say("Already up to date.");
|
|
8319
|
-
|
|
8320
|
-
if (!rt.isTTY)
|
|
8321
|
-
throw new CliError("Not a terminal: pass --yes to accept the plan.", EXIT.invalid);
|
|
8322
|
-
if (!await rt.confirm("Deploy this?"))
|
|
8323
|
-
throw new CliError("Cancelled.", EXIT.invalid);
|
|
8324
|
-
}
|
|
9209
|
+
await confirmOr("Deploy this?", o.yes);
|
|
8325
9210
|
const body = {
|
|
8326
9211
|
sha: plan.sha,
|
|
8327
9212
|
recipeHash: plan.recipeHash,
|
|
8328
|
-
idempotencyKey:
|
|
8329
|
-
...plan.migration.required ? { confirmMigrations: true } : {}
|
|
9213
|
+
idempotencyKey: crypto2.randomBytes(12).toString("hex"),
|
|
9214
|
+
...plan.migration.required && !o.envOnly ? { confirmMigrations: true } : {},
|
|
9215
|
+
...o.envOnly ? { envOnly: true } : {}
|
|
8330
9216
|
};
|
|
8331
9217
|
let sub = null;
|
|
8332
9218
|
for (let i = 0;!sub; i++) {
|
|
@@ -8351,6 +9237,423 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8351
9237
|
}
|
|
8352
9238
|
exit = await finish(api, sub.deploymentId);
|
|
8353
9239
|
});
|
|
9240
|
+
async function planFrom(api, c) {
|
|
9241
|
+
if (!(await checkServer(api)).features.manifest)
|
|
9242
|
+
throw new CliError("This server cannot plan from orbito.json. Update the server: orbito-update", EXIT.invalid);
|
|
9243
|
+
const desired = desiredOf(c);
|
|
9244
|
+
const plan = await api.call("POST", `/apps/${c.appId}/plan`, desired, ManifestPlan);
|
|
9245
|
+
if (g().json)
|
|
9246
|
+
emit({ type: "plan", ...plan });
|
|
9247
|
+
else {
|
|
9248
|
+
if (plan.upToDate)
|
|
9249
|
+
say("Nothing to do: the server matches orbito.json.");
|
|
9250
|
+
for (const x of plan.changes)
|
|
9251
|
+
rt.out(`${x.op === "add" ? "+" : "~"} ${x.what}: ${x.detail}
|
|
9252
|
+
`);
|
|
9253
|
+
for (const b of plan.blockers)
|
|
9254
|
+
rt.out(`! ${b}
|
|
9255
|
+
`);
|
|
9256
|
+
}
|
|
9257
|
+
return { desired, plan };
|
|
9258
|
+
}
|
|
9259
|
+
program.command("plan").description("compare orbito.json with the server; changes nothing").action(async () => {
|
|
9260
|
+
const { c, api } = authed();
|
|
9261
|
+
if ((await planFrom(api, c)).plan.blockers.length > 0)
|
|
9262
|
+
exit = EXIT.failed;
|
|
9263
|
+
});
|
|
9264
|
+
program.command("apply").description("make the server match orbito.json; running it twice changes nothing").option("--yes", "accept the displayed plan without asking (never skips blockers)").action(async (o) => {
|
|
9265
|
+
const { c, api } = authed();
|
|
9266
|
+
const { desired, plan } = await planFrom(api, c);
|
|
9267
|
+
if (plan.blockers.length > 0)
|
|
9268
|
+
throw new CliError("The plan is blocked.", EXIT.failed);
|
|
9269
|
+
if (plan.upToDate)
|
|
9270
|
+
return;
|
|
9271
|
+
await confirmOr("Apply this?", o.yes);
|
|
9272
|
+
const done = await api.call("POST", `/apps/${c.appId}/apply`, { desired, hash: plan.hash });
|
|
9273
|
+
if (g().json)
|
|
9274
|
+
return emit({ type: "applied", ...done });
|
|
9275
|
+
say(`Applied ${done.applied} change(s). They take effect on the next deploy: orbito deploy`);
|
|
9276
|
+
});
|
|
9277
|
+
const serverFor = (explicit) => {
|
|
9278
|
+
if (explicit)
|
|
9279
|
+
return clientFor(new URL(explicit).origin);
|
|
9280
|
+
return clientFor(cfg().server);
|
|
9281
|
+
};
|
|
9282
|
+
const needManifest = async (api) => {
|
|
9283
|
+
if (!(await checkServer(api)).features.manifest)
|
|
9284
|
+
throw new CliError("This server cannot list projects. Update the server: orbito-update", EXIT.invalid);
|
|
9285
|
+
};
|
|
9286
|
+
program.command("projects").description("projects on the server that are not set up as apps yet (administrators)").option("--server <url>", "defaults to the server in orbito.json").action(async (o) => {
|
|
9287
|
+
const api = serverFor(o.server);
|
|
9288
|
+
await needManifest(api);
|
|
9289
|
+
const rows = await api.call("GET", "/projects");
|
|
9290
|
+
if (g().json)
|
|
9291
|
+
return emit(rows);
|
|
9292
|
+
if (rows.length === 0)
|
|
9293
|
+
return say("Every project found on the server is set up.");
|
|
9294
|
+
for (const r of rows)
|
|
9295
|
+
rt.out(`${r.project} ${r.runtimes.join(",") || "not running"} port ${r.suggestedPort ?? "-"}
|
|
9296
|
+
`);
|
|
9297
|
+
});
|
|
9298
|
+
program.command("adopt").description("set a project up as an app. Nothing on the server changes: it is watched until you deploy").argument("<project>", "a name from orbito projects").option("--server <url>", "defaults to the server in orbito.json").action(async (project, o) => {
|
|
9299
|
+
const api = serverFor(o.server);
|
|
9300
|
+
await needManifest(api);
|
|
9301
|
+
const made = await api.call("POST", "/projects/adopt", { project });
|
|
9302
|
+
if (g().json)
|
|
9303
|
+
return emit({ type: "adopted", ...made });
|
|
9304
|
+
say(`'${project}' is now the app '${made.appId}'. It runs as before. Next: orbito init --app ${made.appId}`);
|
|
9305
|
+
});
|
|
9306
|
+
const needBuckets = async (api) => {
|
|
9307
|
+
if (!(await checkServer(api)).features.buckets)
|
|
9308
|
+
throw new CliError("This server has no storage commands. Update the server: orbito-update", EXIT.invalid);
|
|
9309
|
+
};
|
|
9310
|
+
const connection = program.command("storage").description("S3-compatible providers for app files (administrators)").command("connection").description("providers Orbito can create buckets on");
|
|
9311
|
+
connection.command("add").description("add a provider. It is saved only after a real check of every operation").argument("<name>").requiredOption("--endpoint <url>", "for example https://s3.ir-thr-at1.arvanstorage.ir").requiredOption("--region <region>", "for example ir-thr-at1").requiredOption("--access-key <key>", "the access key id").option("--server <url>", "defaults to the server in orbito.json").addHelpText("after", `
|
|
9312
|
+
The secret key is read from stdin, never from an argument:
|
|
9313
|
+
printf %s "$SECRET" | orbito storage connection add arvan --endpoint ... --region ... --access-key ...
|
|
9314
|
+
`).action(async (name, o) => {
|
|
9315
|
+
const api = serverFor(o.server);
|
|
9316
|
+
await needBuckets(api);
|
|
9317
|
+
const raw = rt.isTTY ? await rt.ask("Secret key (shown as you type): ") : await rt.readStdin();
|
|
9318
|
+
const secretKey = raw.trim();
|
|
9319
|
+
if (!secretKey)
|
|
9320
|
+
throw new CliError("Pipe in the secret key.", EXIT.invalid);
|
|
9321
|
+
const made = await api.call("POST", "/storage/connections", {
|
|
9322
|
+
name,
|
|
9323
|
+
endpoint: o.endpoint,
|
|
9324
|
+
region: o.region,
|
|
9325
|
+
accessKey: o.accessKey,
|
|
9326
|
+
secretKey
|
|
9327
|
+
});
|
|
9328
|
+
if (g().json)
|
|
9329
|
+
return emit({ type: "connection", ...made });
|
|
9330
|
+
say(`Checked and saved '${made.name}'. Next: orbito bucket create --connection ${made.name}`);
|
|
9331
|
+
});
|
|
9332
|
+
connection.command("ls").description("names and endpoints; keys are never shown").option("--server <url>", "defaults to the server in orbito.json").action(async (o) => {
|
|
9333
|
+
const api = serverFor(o.server);
|
|
9334
|
+
await needBuckets(api);
|
|
9335
|
+
const rows = await api.call("GET", "/storage/connections", undefined, array(StorageConnection));
|
|
9336
|
+
if (g().json)
|
|
9337
|
+
return emit(rows);
|
|
9338
|
+
for (const r of rows)
|
|
9339
|
+
rt.out(`${r.name} ${r.endpoint} ${r.region} checked ${r.verifiedAt ?? "never"}
|
|
9340
|
+
`);
|
|
9341
|
+
});
|
|
9342
|
+
connection.command("verify").description("run the check again").argument("<name>").option("--server <url>", "defaults to the server in orbito.json").action(async (name, o) => {
|
|
9343
|
+
const api = serverFor(o.server);
|
|
9344
|
+
await needBuckets(api);
|
|
9345
|
+
const done = await api.call("POST", `/storage/connections/${encodeURIComponent(name)}/verify`, {});
|
|
9346
|
+
if (g().json)
|
|
9347
|
+
return emit({ type: "connection", ...done });
|
|
9348
|
+
say(`'${done.name}' passed every step.`);
|
|
9349
|
+
});
|
|
9350
|
+
const bucketCmd = program.command("bucket").description("the app's buckets (sizes are measured, never limited)");
|
|
9351
|
+
bucketCmd.command("ls").action(async () => {
|
|
9352
|
+
const { c, api } = authed();
|
|
9353
|
+
await needBuckets(api);
|
|
9354
|
+
const rows = await api.call("GET", `/apps/${c.appId}/buckets`, undefined, array(AppBucket));
|
|
9355
|
+
if (g().json)
|
|
9356
|
+
return emit(rows);
|
|
9357
|
+
for (const b of rows)
|
|
9358
|
+
rt.out(`${b.binding} ${b.bucket} on ${b.connection} ${b.bytes === null ? "not measured yet" : `${b.bytes} bytes, ${b.objects} objects`}
|
|
9359
|
+
`);
|
|
9360
|
+
});
|
|
9361
|
+
bucketCmd.command("create").description("create a private bucket and bind it to the app as S3_* variables").requiredOption("--connection <name>", "from orbito storage connection ls").option("--binding <name>", "for a second bucket: UPLOADS gives UPLOADS_S3_* variables").action(async (o) => {
|
|
9362
|
+
const { c, api } = authed();
|
|
9363
|
+
await needBuckets(api);
|
|
9364
|
+
const made = await api.call("POST", `/apps/${c.appId}/buckets`, o);
|
|
9365
|
+
if (g().json)
|
|
9366
|
+
return emit({ type: "bucket", ...made });
|
|
9367
|
+
say(`Created ${made.bucket}, bound as ${made.binding}. It takes effect on the next deploy: orbito deploy --env-only`);
|
|
9368
|
+
info(`Note: ${made.note}`);
|
|
9369
|
+
});
|
|
9370
|
+
bucketCmd.command("remove").description("unbind a bucket. The bucket and its files stay at the provider").argument("<binding>").option("--confirm <binding>", "the binding again, for scripts").action(async (binding, o) => {
|
|
9371
|
+
const { c, api } = authed();
|
|
9372
|
+
await needBuckets(api);
|
|
9373
|
+
let typed = o.confirm;
|
|
9374
|
+
if (!typed) {
|
|
9375
|
+
if (!rt.isTTY)
|
|
9376
|
+
throw new CliError(`Not a terminal: pass --confirm ${binding}.`, EXIT.confirm);
|
|
9377
|
+
typed = (await rt.ask(`Type ${binding} to confirm: `)).trim();
|
|
9378
|
+
}
|
|
9379
|
+
if (typed !== binding)
|
|
9380
|
+
throw new CliError("It did not match. Nothing was changed.", EXIT.confirm);
|
|
9381
|
+
const done = await api.call("DELETE", `/apps/${c.appId}/buckets/${encodeURIComponent(binding)}?confirm=${encodeURIComponent(typed)}`);
|
|
9382
|
+
if (g().json)
|
|
9383
|
+
return emit({ type: "bucket.removed", binding, note: done.note });
|
|
9384
|
+
say(`Unbound ${binding}. ${done.note}`);
|
|
9385
|
+
});
|
|
9386
|
+
const environmentCmd = program.command("environment").description("the app's other environments, such as staging");
|
|
9387
|
+
const needEnvironments = async (api) => {
|
|
9388
|
+
if (!(await checkServer(api)).features.environments)
|
|
9389
|
+
throw new CliError("This server has no environments. Update the server: orbito-update", EXIT.invalid);
|
|
9390
|
+
};
|
|
9391
|
+
environmentCmd.command("ls").action(async () => {
|
|
9392
|
+
const { c, api } = authed();
|
|
9393
|
+
await needEnvironments(api);
|
|
9394
|
+
const rows = await api.call("GET", `/apps/${c.appId}/environments`, undefined, array(AppEnvironment));
|
|
9395
|
+
if (g().json)
|
|
9396
|
+
return emit(rows);
|
|
9397
|
+
if (rows.length === 0)
|
|
9398
|
+
say("Only production so far.");
|
|
9399
|
+
for (const e of rows)
|
|
9400
|
+
rt.out(`${e.environment} ${e.id} port ${e.port ?? "-"} ${e.deployedSha?.slice(0, 12) ?? "not deployed"}
|
|
9401
|
+
`);
|
|
9402
|
+
});
|
|
9403
|
+
environmentCmd.command("create").description("add an environment: its own folder, user, port, database and variables (administrators)").argument("<name>", "for example staging").option("--port <port>", "the port it listens on; omit to let Orbito pick a free one").option("--branch <branch>", "the branch it deploys; defaults to the same as production").action(async (name, o) => {
|
|
9404
|
+
const { c, api } = authed();
|
|
9405
|
+
await needEnvironments(api);
|
|
9406
|
+
const made = await api.call("POST", `/apps/${c.appId}/environments`, {
|
|
9407
|
+
name,
|
|
9408
|
+
...o.port ? { port: Number(o.port) } : {},
|
|
9409
|
+
...o.branch ? { branch: o.branch } : {}
|
|
9410
|
+
});
|
|
9411
|
+
if (g().json)
|
|
9412
|
+
return emit({ type: "environment", ...made });
|
|
9413
|
+
say(`Created '${made.id}' on port ${made.port}. Copied ${made.copied.length} plain variable(s); production's secrets and database were not copied.`);
|
|
9414
|
+
for (const n of made.secretsToSet)
|
|
9415
|
+
say(` set it: orbito env set ${n} --secret --env ${name}`);
|
|
9416
|
+
say(`Next: orbito deploy --env ${name}`);
|
|
9417
|
+
});
|
|
9418
|
+
program.command("export").description("write files to run the app without Orbito: Dockerfile, Compose, a systemd unit, .env.example").option("--out <dir>", "an empty or new folder", "orbito-export").action(async (o) => {
|
|
9419
|
+
const { c, api } = authed();
|
|
9420
|
+
if (!(await checkServer(api)).features.export)
|
|
9421
|
+
throw new CliError("This server cannot export. Update the server: orbito-update", EXIT.invalid);
|
|
9422
|
+
const dir = path4.resolve(rt.cwd, o.out);
|
|
9423
|
+
if (fs4.existsSync(dir) && fs4.readdirSync(dir).length > 0)
|
|
9424
|
+
throw new CliError(`${dir} is not empty. Nothing was written.`, EXIT.invalid);
|
|
9425
|
+
const { files } = await api.call("GET", `/apps/${c.appId}/export`);
|
|
9426
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
9427
|
+
for (const [name, text] of Object.entries(files)) {
|
|
9428
|
+
if (name !== path4.basename(name))
|
|
9429
|
+
throw new CliError(`The server sent an odd file name: ${name}`, EXIT.failed);
|
|
9430
|
+
fs4.writeFileSync(path4.join(dir, name), text);
|
|
9431
|
+
}
|
|
9432
|
+
if (g().json)
|
|
9433
|
+
return emit({ type: "export", dir, files: Object.keys(files) });
|
|
9434
|
+
say(`Wrote ${Object.keys(files).length} files to ${dir}. They hold variable names, never values.`);
|
|
9435
|
+
});
|
|
9436
|
+
program.command("licence").description("the plan, the limit and how many apps count").option("--server <url>", "defaults to the server in orbito.json").action(async (o) => {
|
|
9437
|
+
const api = serverFor(o.server);
|
|
9438
|
+
if (!(await checkServer(api)).features.licence)
|
|
9439
|
+
throw new CliError("This server does not report a licence. Update the server: orbito-update", EXIT.invalid);
|
|
9440
|
+
const s = await api.call("GET", "/licence", undefined, LicenceState);
|
|
9441
|
+
if (g().json)
|
|
9442
|
+
return emit(s);
|
|
9443
|
+
rt.out(`${s.plan}${s.customer ? ` for ${s.customer}` : ""}: ${s.used} of ${s.maxApps ?? "unlimited"} apps
|
|
9444
|
+
`);
|
|
9445
|
+
if (s.expires)
|
|
9446
|
+
rt.out(`Expires ${s.expires.slice(0, 10)}
|
|
9447
|
+
`);
|
|
9448
|
+
if (s.problem)
|
|
9449
|
+
rt.out(`${s.problem}
|
|
9450
|
+
`);
|
|
9451
|
+
});
|
|
9452
|
+
const domainCmd = program.command("domain").description("the app's domains, DNS and certificates");
|
|
9453
|
+
const needDomains = async (api) => {
|
|
9454
|
+
if (!(await checkServer(api)).features.domains)
|
|
9455
|
+
throw new CliError("This server has no domain commands. Update the server: orbito-update", EXIT.invalid);
|
|
9456
|
+
};
|
|
9457
|
+
const showDomains = (list) => {
|
|
9458
|
+
if (g().json)
|
|
9459
|
+
return emit(list);
|
|
9460
|
+
for (const d of list.domains)
|
|
9461
|
+
rt.out(`${d.domain} ${d.stage}
|
|
9462
|
+
${d.detail}
|
|
9463
|
+
`);
|
|
9464
|
+
if (list.applied?.length)
|
|
9465
|
+
rt.out(`Routed: ${list.applied.join(", ")}
|
|
9466
|
+
`);
|
|
9467
|
+
if (list.note)
|
|
9468
|
+
rt.out(`${list.note}
|
|
9469
|
+
`);
|
|
9470
|
+
};
|
|
9471
|
+
domainCmd.command("ls").description("each domain and how far along it is").action(async () => {
|
|
9472
|
+
const { c, api } = authed();
|
|
9473
|
+
await needDomains(api);
|
|
9474
|
+
showDomains(await api.call("GET", `/apps/${c.appId}/domains`, undefined, DomainList));
|
|
9475
|
+
});
|
|
9476
|
+
domainCmd.command("add").description("add a domain; it is routed once its DNS points here").argument("<domain>").action(async (domain) => {
|
|
9477
|
+
const { c, api } = authed();
|
|
9478
|
+
await needDomains(api);
|
|
9479
|
+
showDomains(await api.call("POST", `/apps/${c.appId}/domains`, { domain }, DomainList));
|
|
9480
|
+
});
|
|
9481
|
+
domainCmd.command("check").description("check DNS again and route what is ready").action(async () => {
|
|
9482
|
+
const { c, api } = authed();
|
|
9483
|
+
await needDomains(api);
|
|
9484
|
+
showDomains(await api.call("POST", `/apps/${c.appId}/domains/check`, {}, DomainList));
|
|
9485
|
+
});
|
|
9486
|
+
domainCmd.command("remove").description("remove a domain; needs the domain typed again, or --confirm <domain>").argument("<domain>").option("--dry-run", "say what would happen; change nothing").option("--confirm <domain>", "the domain again, for scripts").action(async (domain, o) => {
|
|
9487
|
+
const { c, api } = authed();
|
|
9488
|
+
await needDomains(api);
|
|
9489
|
+
say(`This stops '${c.appId}' answering on ${domain}.`);
|
|
9490
|
+
if (o.dryRun)
|
|
9491
|
+
return;
|
|
9492
|
+
let typed = o.confirm;
|
|
9493
|
+
if (!typed) {
|
|
9494
|
+
if (!rt.isTTY)
|
|
9495
|
+
throw new CliError(`Not a terminal: pass --confirm ${domain}.`, EXIT.confirm);
|
|
9496
|
+
typed = (await rt.ask(`Type ${domain} to confirm: `)).trim();
|
|
9497
|
+
}
|
|
9498
|
+
if (typed !== domain)
|
|
9499
|
+
throw new CliError("It did not match. Nothing was changed.", EXIT.confirm);
|
|
9500
|
+
const done = await api.call("DELETE", `/apps/${c.appId}/domains/${encodeURIComponent(domain)}`);
|
|
9501
|
+
if (g().json)
|
|
9502
|
+
return emit({ type: "domain.removed", domain, note: done.note });
|
|
9503
|
+
say(`Removed ${domain}.${done.note ? ` ${done.note}` : ""}`);
|
|
9504
|
+
});
|
|
9505
|
+
program.command("dev").description("start the local database, migrate, and run the dev script. Needs no Orbito server").option("--setup-only", "stop before running the dev script").action(async (o) => {
|
|
9506
|
+
const c = cfg();
|
|
9507
|
+
const database = await setupDatabase(rt, c);
|
|
9508
|
+
if (database.engine === "none")
|
|
9509
|
+
info('No local database: add "dev": { "database": "sqlite" } to orbito.json to get one.');
|
|
9510
|
+
else
|
|
9511
|
+
info(`Local database: ${database.engine}`);
|
|
9512
|
+
const migrated = await runScript(rt, "db:migrate");
|
|
9513
|
+
if (migrated)
|
|
9514
|
+
throw new CliError("db:migrate failed.", EXIT.failed);
|
|
9515
|
+
if (o.setupOnly)
|
|
9516
|
+
return;
|
|
9517
|
+
const code = await runScript(rt, "dev");
|
|
9518
|
+
if (code === null)
|
|
9519
|
+
throw new CliError("package.json has no dev script.", EXIT.invalid);
|
|
9520
|
+
exit = code;
|
|
9521
|
+
});
|
|
9522
|
+
program.command("usage").description("what the app uses: memory, CPU, processes, restarts, disk (never limited)").option("--range <range>", "24h, 7d or 30d", "24h").action(async (o) => {
|
|
9523
|
+
const { c, api } = authed();
|
|
9524
|
+
if (!(await checkServer(api)).features.usage)
|
|
9525
|
+
throw new CliError("This server does not measure usage. Update the server: orbito-update", EXIT.invalid);
|
|
9526
|
+
const u = await api.call("GET", `/apps/${c.appId}/usage?range=${encodeURIComponent(o.range)}`, undefined, AppUsage);
|
|
9527
|
+
if (g().json)
|
|
9528
|
+
return emit(u);
|
|
9529
|
+
if (!u.measured)
|
|
9530
|
+
return say("No usage recorded yet. Only apps run by systemd are measured so far.");
|
|
9531
|
+
const mb = (n) => n === null ? "-" : `${(n / 1048576).toFixed(0)} MB`;
|
|
9532
|
+
rt.out(`${c.appId} at ${u.at}
|
|
9533
|
+
`);
|
|
9534
|
+
rt.out(` memory ${mb(u.memoryBytes)} (peak ${mb(u.memoryPeakBytes)} in ${u.range})
|
|
9535
|
+
`);
|
|
9536
|
+
rt.out(` cpu ${u.cpuPercent ?? "-"}% of one core
|
|
9537
|
+
`);
|
|
9538
|
+
rt.out(` processes ${u.pids ?? "-"}
|
|
9539
|
+
`);
|
|
9540
|
+
rt.out(` restarts ${u.restarts ?? "-"}, out of memory ${u.oomKills ?? "-"}
|
|
9541
|
+
`);
|
|
9542
|
+
rt.out(` disk ${mb(u.diskBytes)}
|
|
9543
|
+
`);
|
|
9544
|
+
if (u.p50Ms != null)
|
|
9545
|
+
rt.out(` response p50 ${u.p50Ms} ms, p95 ${u.p95Ms} ms, ${u.requests} requests, ${u.errors} server errors in ${u.range}
|
|
9546
|
+
`);
|
|
9547
|
+
for (const a of u.alerts ?? [])
|
|
9548
|
+
rt.out(` ALERT ${a.message}
|
|
9549
|
+
`);
|
|
9550
|
+
});
|
|
9551
|
+
const envCmd = program.command("env").description("variables Orbito holds for the app");
|
|
9552
|
+
const needEnvCommands = async (api) => {
|
|
9553
|
+
if (!(await checkServer(api)).features.envCommands)
|
|
9554
|
+
throw new CliError("This server has no env commands. Update the server: orbito-update", EXIT.invalid);
|
|
9555
|
+
};
|
|
9556
|
+
envCmd.command("pull").description("copy variables to .env.local. Secrets are not copied: development uses its own").action(async () => {
|
|
9557
|
+
const { c, api } = authed();
|
|
9558
|
+
if (!(await checkServer(api)).features.envValues)
|
|
9559
|
+
throw new CliError("This server cannot do this. Update the server: orbito-update", EXIT.invalid);
|
|
9560
|
+
await ensureIgnored(rt);
|
|
9561
|
+
const rows = await api.call("GET", `/apps/${c.appId}/env?values=plain`, undefined, EnvList);
|
|
9562
|
+
const have = readEnvLocal(rt);
|
|
9563
|
+
const pulled = rows.filter((v) => !v.secret && v.value !== undefined && v.name !== "DATABASE_URL");
|
|
9564
|
+
writeEnvLocal(rt, Object.fromEntries(pulled.map((v) => [v.name, v.value])));
|
|
9565
|
+
const missing = rows.filter((v) => v.secret && !(v.name in have) && v.name !== "DATABASE_URL").map((v) => v.name);
|
|
9566
|
+
if (g().json)
|
|
9567
|
+
return emit({ type: "env.pull", pulled: pulled.map((v) => v.name), secretsToSet: missing });
|
|
9568
|
+
say(`Wrote ${pulled.length} variable(s) to .env.local.`);
|
|
9569
|
+
if (missing.length)
|
|
9570
|
+
say(`Secrets to set yourself in .env.local: ${missing.join(", ")}`);
|
|
9571
|
+
});
|
|
9572
|
+
envCmd.command("diff").description("names that differ between .env.local and the server; values are never shown").action(async () => {
|
|
9573
|
+
const { c, api } = authed();
|
|
9574
|
+
await needEnvCommands(api);
|
|
9575
|
+
const rows = await api.call("GET", `/apps/${c.appId}/env`, undefined, EnvList);
|
|
9576
|
+
const local = new Set(Object.keys(readEnvLocal(rt)));
|
|
9577
|
+
const server = new Set(rows.map((r) => r.name));
|
|
9578
|
+
const onlyLocal = [...local].filter((n) => !server.has(n)).sort();
|
|
9579
|
+
const onlyServer = [...server].filter((n) => !local.has(n)).sort();
|
|
9580
|
+
if (g().json)
|
|
9581
|
+
return emit({ type: "env.diff", onlyLocal, onlyServer });
|
|
9582
|
+
for (const n of onlyLocal)
|
|
9583
|
+
rt.out(`local only ${n}
|
|
9584
|
+
`);
|
|
9585
|
+
for (const n of onlyServer)
|
|
9586
|
+
rt.out(`server only ${n}
|
|
9587
|
+
`);
|
|
9588
|
+
if (!onlyLocal.length && !onlyServer.length)
|
|
9589
|
+
say("The same names on both sides.");
|
|
9590
|
+
});
|
|
9591
|
+
envCmd.command("push").description("add variables from a file to the server. Never replaces one unless --overwrite").argument("<file>", "an env file").option("--overwrite", "also replace variables the server already has").option("--dry-run", "list what would change; change nothing").option("--yes", "do not ask").action(async (file, o) => {
|
|
9592
|
+
const { c, api } = authed();
|
|
9593
|
+
await needEnvCommands(api);
|
|
9594
|
+
let text;
|
|
9595
|
+
try {
|
|
9596
|
+
text = fs4.readFileSync(path4.resolve(rt.cwd, file), "utf8");
|
|
9597
|
+
} catch {
|
|
9598
|
+
throw new CliError(`Cannot read ${file}.`, EXIT.invalid);
|
|
9599
|
+
}
|
|
9600
|
+
const rows = await api.call("GET", `/apps/${c.appId}/env`, undefined, EnvList);
|
|
9601
|
+
const server = new Set(rows.map((r) => r.name));
|
|
9602
|
+
const all = Object.entries(parseEnv(text)).filter(([n]) => n !== "DATABASE_URL");
|
|
9603
|
+
const send = all.filter(([n]) => o.overwrite || !server.has(n));
|
|
9604
|
+
for (const [n] of send)
|
|
9605
|
+
rt[g().json ? "err" : "out"](`${server.has(n) ? "~" : "+"} ${n}
|
|
9606
|
+
`);
|
|
9607
|
+
const skipped = all.length - send.length;
|
|
9608
|
+
if (skipped)
|
|
9609
|
+
info(`Left ${skipped} the server already has. Pass --overwrite to replace them.`);
|
|
9610
|
+
if (o.dryRun || send.length === 0)
|
|
9611
|
+
return;
|
|
9612
|
+
await confirmOr("Send these to the server?", o.yes);
|
|
9613
|
+
for (const [name, value] of send)
|
|
9614
|
+
await api.call("POST", `/apps/${c.appId}/env`, { name, value });
|
|
9615
|
+
if (g().json)
|
|
9616
|
+
return emit({ type: "env.push", names: send.map(([n]) => n) });
|
|
9617
|
+
say(`Sent ${send.length}. They take effect on the next deploy: orbito deploy --env-only`);
|
|
9618
|
+
});
|
|
9619
|
+
envCmd.command("ls").description("names only; values are never printed").action(async () => {
|
|
9620
|
+
const { c, api } = authed();
|
|
9621
|
+
await needEnvCommands(api);
|
|
9622
|
+
const rows = await api.call("GET", `/apps/${c.appId}/env`, undefined, EnvList);
|
|
9623
|
+
if (g().json)
|
|
9624
|
+
return emit(rows);
|
|
9625
|
+
for (const v of rows)
|
|
9626
|
+
rt.out(`${v.name}${v.secret ? " (secret)" : ""} ${v.updatedAt}
|
|
9627
|
+
`);
|
|
9628
|
+
});
|
|
9629
|
+
envCmd.command("set").description("set a variable; the value comes from stdin, never from an argument").argument("<name>").option("--secret", "treat it as a secret").addHelpText("after", `
|
|
9630
|
+
Example:
|
|
9631
|
+
printf %s "$VALUE" | orbito env set API_KEY --secret
|
|
9632
|
+
`).action(async (name, o) => {
|
|
9633
|
+
const { c, api } = authed();
|
|
9634
|
+
await needEnvCommands(api);
|
|
9635
|
+
const raw = rt.isTTY ? await rt.ask(`Value for ${name} (shown as you type): `) : await rt.readStdin();
|
|
9636
|
+
const value = raw.replace(/\r?\n$/, "");
|
|
9637
|
+
await api.call("POST", `/apps/${c.appId}/env`, {
|
|
9638
|
+
name,
|
|
9639
|
+
value,
|
|
9640
|
+
...o.secret ? { secret: true } : {}
|
|
9641
|
+
});
|
|
9642
|
+
say(`Set ${name}. It takes effect on the next deploy: orbito deploy --env-only`);
|
|
9643
|
+
if (g().json)
|
|
9644
|
+
emit({ type: "env.set", name });
|
|
9645
|
+
});
|
|
9646
|
+
envCmd.command("unset").description("remove a variable from what Orbito holds").argument("<name>").option("--dry-run", "say what would be removed; change nothing").option("--yes", "do not ask").action(async (name, o) => {
|
|
9647
|
+
const { c, api } = authed();
|
|
9648
|
+
await needEnvCommands(api);
|
|
9649
|
+
if (o.dryRun)
|
|
9650
|
+
return say(`Would remove ${name}.`);
|
|
9651
|
+
await confirmOr(`Remove ${name}?`, o.yes);
|
|
9652
|
+
await api.call("DELETE", `/apps/${c.appId}/env/${encodeURIComponent(name)}`);
|
|
9653
|
+
say(`Removed ${name} from Orbito. A deploy does not delete it from the .env already on the server.`);
|
|
9654
|
+
if (g().json)
|
|
9655
|
+
emit({ type: "env.unset", name });
|
|
9656
|
+
});
|
|
8354
9657
|
const dep = program.command("deployments").description("deployment history");
|
|
8355
9658
|
dep.command("list").description("recent deployments").option("--limit <n>", "how many", "10").action(async (o) => {
|
|
8356
9659
|
const { c, api } = authed();
|
|
@@ -8361,7 +9664,7 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8361
9664
|
rt.out(`${d.id} ${d.startedAt} ${d.kind} ${d.status} ${d.sha?.slice(0, 12) ?? "-"} ${d.actor}
|
|
8362
9665
|
`);
|
|
8363
9666
|
});
|
|
8364
|
-
program.command("tail").description("follow runtime logs, or a deployment with --deployment").option("--deployment <id>", "follow a deployment and migration output instead").option("--lines <n>", "lines of history first (0-500)", "100").option("--since <iso>", "continue after this time (container logs only)").action(async (o) => {
|
|
9667
|
+
program.command("tail").alias("logs").description("follow runtime logs, or a deployment with --deployment").option("--deployment <id>", "follow a deployment and migration output instead").option("--lines <n>", "lines of history first (0-500)", "100").option("--since <iso>", "continue after this time (container logs only)").action(async (o) => {
|
|
8365
9668
|
const { c, api } = authed();
|
|
8366
9669
|
if (o.deployment) {
|
|
8367
9670
|
if (await follow(api, o.deployment) === "interrupted")
|
|
@@ -8434,19 +9737,129 @@ Next: orbito doctor then orbito deploy --dry-run`);
|
|
|
8434
9737
|
`);
|
|
8435
9738
|
for (const r of s.recent)
|
|
8436
9739
|
rt.out(` ${r.at} ${r.deploymentId} ${r.migration} backup ${r.backup ?? "-"}
|
|
9740
|
+
`);
|
|
9741
|
+
});
|
|
9742
|
+
db.command("copy").description("replace an environment's data with a fresh copy of production's (use with --env)").option("--confirm <id>", "the environment id, for scripts; --yes is not accepted here").action(async (o) => {
|
|
9743
|
+
const { c, api } = authed();
|
|
9744
|
+
await needEnvironments(api);
|
|
9745
|
+
say(`This replaces all data of '${c.appId}' with a copy of production's. Real data will be in this environment. A safety dump is taken first.`);
|
|
9746
|
+
let typed = o.confirm;
|
|
9747
|
+
if (!typed) {
|
|
9748
|
+
if (!rt.isTTY)
|
|
9749
|
+
throw new CliError(`Not a terminal: pass --confirm ${c.appId}.`, EXIT.confirm);
|
|
9750
|
+
typed = (await rt.ask(`Type ${c.appId} to confirm: `)).trim();
|
|
9751
|
+
}
|
|
9752
|
+
if (typed !== c.appId)
|
|
9753
|
+
throw new CliError("It did not match. Nothing was changed.", EXIT.confirm);
|
|
9754
|
+
const done = await api.call("POST", `/apps/${c.appId}/db/copy`, { confirm: typed });
|
|
9755
|
+
if (g().json)
|
|
9756
|
+
return emit({ type: "db.copy", ...done });
|
|
9757
|
+
rt.out(`Copied from ${done.from}. Safety dump: ${done.safety}
|
|
9758
|
+
`);
|
|
9759
|
+
});
|
|
9760
|
+
db.command("seed").description("local: run the db:seed script against the local database").action(async () => {
|
|
9761
|
+
const code = await runScript(rt, "db:seed");
|
|
9762
|
+
if (code === null)
|
|
9763
|
+
throw new CliError("package.json has no db:seed script.", EXIT.invalid);
|
|
9764
|
+
exit = code;
|
|
9765
|
+
});
|
|
9766
|
+
db.command("studio").description("local: run the db:studio script against the local database").action(async () => {
|
|
9767
|
+
const code = await runScript(rt, "db:studio");
|
|
9768
|
+
if (code === null)
|
|
9769
|
+
throw new CliError("package.json has no db:studio script.", EXIT.invalid);
|
|
9770
|
+
exit = code;
|
|
9771
|
+
});
|
|
9772
|
+
db.command("shell").description("local: open a shell on the local database").action(async () => {
|
|
9773
|
+
const [cmd, args] = shellCommand(cfg(), rt.cwd);
|
|
9774
|
+
exit = (await rt.run(cmd, args)).code;
|
|
9775
|
+
});
|
|
9776
|
+
db.command("reset").description("local: delete the local database, then migrate and seed. Never touches a server").option("--confirm <app-id>", "the app id, for scripts").action(async (o) => {
|
|
9777
|
+
const c = cfg();
|
|
9778
|
+
let typed = o.confirm;
|
|
9779
|
+
if (!typed) {
|
|
9780
|
+
if (!rt.isTTY)
|
|
9781
|
+
throw new CliError(`Not a terminal: pass --confirm ${c.appId}.`, EXIT.confirm);
|
|
9782
|
+
typed = (await rt.ask(`This deletes the local database. Type ${c.appId} to confirm: `)).trim();
|
|
9783
|
+
}
|
|
9784
|
+
if (typed !== c.appId)
|
|
9785
|
+
throw new CliError("The app id did not match. Nothing was changed.", EXIT.confirm);
|
|
9786
|
+
await dropDatabase(rt, c);
|
|
9787
|
+
await setupDatabase(rt, c);
|
|
9788
|
+
for (const script of ["db:migrate", "db:seed"]) {
|
|
9789
|
+
const code = await runScript(rt, script);
|
|
9790
|
+
if (code)
|
|
9791
|
+
throw new CliError(`${script} failed.`, EXIT.failed);
|
|
9792
|
+
}
|
|
9793
|
+
say("The local database was recreated.");
|
|
9794
|
+
});
|
|
9795
|
+
const needDbBackups = async (api) => {
|
|
9796
|
+
if (!(await checkServer(api)).features.dbBackups)
|
|
9797
|
+
throw new CliError("This server has no database backup commands. Update the server: orbito-update", EXIT.invalid);
|
|
9798
|
+
};
|
|
9799
|
+
db.command("backup").description("take a dump of the database now").action(async () => {
|
|
9800
|
+
const { c, api } = authed();
|
|
9801
|
+
await needDbBackups(api);
|
|
9802
|
+
const made = await api.call("POST", `/apps/${c.appId}/db/backup`, {});
|
|
9803
|
+
if (g().json)
|
|
9804
|
+
return emit({ type: "backup", ...made });
|
|
9805
|
+
rt.out(`${made.file} (${made.bytes} bytes)
|
|
9806
|
+
`);
|
|
9807
|
+
});
|
|
9808
|
+
db.command("dumps").description("the dumps kept on the server").action(async () => {
|
|
9809
|
+
const { c, api } = authed();
|
|
9810
|
+
await needDbBackups(api);
|
|
9811
|
+
const rows = await api.call("GET", `/apps/${c.appId}/db/dumps`);
|
|
9812
|
+
if (g().json)
|
|
9813
|
+
return emit(rows);
|
|
9814
|
+
for (const d of rows)
|
|
9815
|
+
rt.out(`${d.name} ${d.bytes} bytes ${d.at}
|
|
9816
|
+
`);
|
|
9817
|
+
});
|
|
9818
|
+
db.command("restore").description("replace the database with a kept dump (a safety dump is taken first; needs the app id typed)").argument("<file>", "a name from orbito db dumps").option("--dry-run", "say what would happen; change nothing").option("--confirm <app-id>", "the app id, for scripts; --yes is not accepted here").action(async (file, o) => {
|
|
9819
|
+
const { c, api } = authed();
|
|
9820
|
+
await needDbBackups(api);
|
|
9821
|
+
say(`This replaces all data of '${c.appId}' with ${file}. A safety dump is taken first.`);
|
|
9822
|
+
if (o.dryRun)
|
|
9823
|
+
return;
|
|
9824
|
+
let typed = o.confirm;
|
|
9825
|
+
if (!typed) {
|
|
9826
|
+
if (!rt.isTTY)
|
|
9827
|
+
throw new CliError(`Not a terminal: pass --confirm ${c.appId}.`, EXIT.confirm);
|
|
9828
|
+
typed = (await rt.ask(`Type ${c.appId} to confirm: `)).trim();
|
|
9829
|
+
}
|
|
9830
|
+
if (typed !== c.appId)
|
|
9831
|
+
throw new CliError("The app id did not match. Nothing was changed.", EXIT.confirm);
|
|
9832
|
+
const done = await api.call("POST", `/apps/${c.appId}/db/restore`, { file, confirm: typed });
|
|
9833
|
+
if (g().json)
|
|
9834
|
+
return emit({ type: "restore", ...done });
|
|
9835
|
+
rt.out(`Restored ${done.restored}. Safety dump: ${done.safety}
|
|
8437
9836
|
`);
|
|
8438
9837
|
});
|
|
8439
9838
|
db.command("migrate").description("apply migrations from a deployment (refused unless the server can prove it is safe)").requiredOption("--deployment <id>", "the deployment whose source to migrate from").action(async () => {
|
|
8440
9839
|
const { c, api } = authed();
|
|
8441
9840
|
await api.call("POST", `/apps/${c.appId}/db/migrate`, {});
|
|
8442
9841
|
});
|
|
8443
|
-
program.command("rollback").description("restore the earlier code release (refused if the database changed since)").requiredOption("--deployment <id>", "the latest successful deployment, which is undone").option("--detach", "return the deployment id without following").action(async (o) => {
|
|
9842
|
+
program.command("rollback").description("restore the earlier code release (refused if the database changed since)").requiredOption("--deployment <id>", "the latest successful deployment, which is undone").option("--dry-run", "show what would be undone, or why it is refused; change nothing").option("--yes", "accept the displayed plan without asking").option("--detach", "return the deployment id without following").action(async (o) => {
|
|
8444
9843
|
const { c, api } = authed();
|
|
9844
|
+
if (!(await checkServer(api)).features.rollbackPlan)
|
|
9845
|
+
throw new CliError("This server cannot plan a rollback. Update the server: orbito-update", EXIT.invalid);
|
|
9846
|
+
const plan = await api.call("POST", `/apps/${c.appId}/rollback/plan`, { deploymentId: o.deployment }, Deployment);
|
|
9847
|
+
if (g().json)
|
|
9848
|
+
emit({ type: "plan", ...plan });
|
|
9849
|
+
else
|
|
9850
|
+
rt.out(`Roll back ${plan.id}: ${plan.sha?.slice(0, 12) ?? "unknown"} -> ${plan.from?.slice(0, 12) ?? "the earlier release"}
|
|
9851
|
+
`);
|
|
9852
|
+
if (o.dryRun)
|
|
9853
|
+
return;
|
|
9854
|
+
await confirmOr("Roll back?", o.yes);
|
|
8445
9855
|
const sub = await api.call("POST", `/apps/${c.appId}/rollback`, {
|
|
8446
|
-
idempotencyKey:
|
|
9856
|
+
idempotencyKey: crypto2.randomBytes(12).toString("hex"),
|
|
8447
9857
|
deploymentId: o.deployment
|
|
8448
9858
|
});
|
|
8449
|
-
|
|
9859
|
+
if (g().json)
|
|
9860
|
+
emit({ type: "submitted", ...sub });
|
|
9861
|
+
else
|
|
9862
|
+
say(`Submitted ${sub.deploymentId}.`);
|
|
8450
9863
|
if (o.detach)
|
|
8451
9864
|
return;
|
|
8452
9865
|
if (await follow(api, sub.deploymentId) === "interrupted")
|
|
@@ -8555,5 +9968,55 @@ async function readSse(res, signal, on) {
|
|
|
8555
9968
|
}
|
|
8556
9969
|
}
|
|
8557
9970
|
|
|
9971
|
+
// src/update.ts
|
|
9972
|
+
import fs5 from "node:fs";
|
|
9973
|
+
import path5 from "node:path";
|
|
9974
|
+
var DAY = 24 * 60 * 60 * 1000;
|
|
9975
|
+
var newer = (a, b) => {
|
|
9976
|
+
const p = (v) => v.split("-")[0].split(".").map((n) => Number(n) || 0);
|
|
9977
|
+
const [x, y] = [p(a), p(b)];
|
|
9978
|
+
for (let i = 0;i < 3; i++)
|
|
9979
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0))
|
|
9980
|
+
return (x[i] ?? 0) > (y[i] ?? 0);
|
|
9981
|
+
return false;
|
|
9982
|
+
};
|
|
9983
|
+
async function updateNotice(rt, name, current) {
|
|
9984
|
+
if (!rt.isTTY || rt.env.CI || rt.env.ORBITO_NO_UPDATE_CHECK)
|
|
9985
|
+
return null;
|
|
9986
|
+
const cache = path5.join(rt.configDir, "update-check.json");
|
|
9987
|
+
let latest;
|
|
9988
|
+
try {
|
|
9989
|
+
const c = JSON.parse(fs5.readFileSync(cache, "utf8"));
|
|
9990
|
+
if (Date.now() - c.at < DAY)
|
|
9991
|
+
latest = c.latest;
|
|
9992
|
+
} catch {}
|
|
9993
|
+
if (!latest) {
|
|
9994
|
+
try {
|
|
9995
|
+
const registry = (rt.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/$/, "");
|
|
9996
|
+
const res = await rt.fetch(`${registry}/${name.replace("/", "%2f")}/latest`, {
|
|
9997
|
+
signal: AbortSignal.timeout(1500)
|
|
9998
|
+
});
|
|
9999
|
+
if (!res.ok)
|
|
10000
|
+
return null;
|
|
10001
|
+
latest = (await res.json()).version;
|
|
10002
|
+
if (!latest)
|
|
10003
|
+
return null;
|
|
10004
|
+
fs5.mkdirSync(rt.configDir, { recursive: true, mode: 448 });
|
|
10005
|
+
fs5.writeFileSync(cache, JSON.stringify({ at: Date.now(), latest }));
|
|
10006
|
+
} catch {
|
|
10007
|
+
return null;
|
|
10008
|
+
}
|
|
10009
|
+
}
|
|
10010
|
+
return newer(latest, current) ? `Update available: ${current} → ${latest}. Run: bun add --dev --exact ${name}@latest` : null;
|
|
10011
|
+
}
|
|
10012
|
+
|
|
8558
10013
|
// src/main.ts
|
|
8559
|
-
|
|
10014
|
+
var rt = realRuntime();
|
|
10015
|
+
var argv = process2.argv.slice(2);
|
|
10016
|
+
var notice = argv.includes("--json") ? Promise.resolve(null) : updateNotice(rt, "@deaduchiha/orbito", VERSION);
|
|
10017
|
+
process2.exitCode = await run(argv, rt);
|
|
10018
|
+
var n = await notice;
|
|
10019
|
+
if (n)
|
|
10020
|
+
rt.err(`
|
|
10021
|
+
${n}
|
|
10022
|
+
`);
|