@deaduchiha/orbito 0.1.0 → 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 +1650 -130
- 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.0",
|
|
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";
|
|
@@ -7845,6 +8275,44 @@ function realRuntime() {
|
|
|
7845
8275
|
resolve(/^y(es)?$/i.test(a.trim()));
|
|
7846
8276
|
});
|
|
7847
8277
|
}),
|
|
8278
|
+
ask: (question) => new Promise((resolve) => {
|
|
8279
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8280
|
+
rl.question(question, (a) => {
|
|
8281
|
+
rl.close();
|
|
8282
|
+
resolve(a);
|
|
8283
|
+
});
|
|
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
|
+
},
|
|
7848
8316
|
onInterrupt: (fn) => {
|
|
7849
8317
|
process.on("SIGINT", fn);
|
|
7850
8318
|
return () => process.off("SIGINT", fn);
|
|
@@ -7893,13 +8361,28 @@ function loadToken(rt, server) {
|
|
|
7893
8361
|
return null;
|
|
7894
8362
|
return c.token;
|
|
7895
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
|
+
}
|
|
7896
8379
|
|
|
7897
8380
|
// src/client.ts
|
|
7898
8381
|
class ApiFailure extends CliError {
|
|
7899
8382
|
status;
|
|
7900
8383
|
code;
|
|
7901
8384
|
constructor(message, status, code) {
|
|
7902
|
-
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);
|
|
7903
8386
|
this.status = status;
|
|
7904
8387
|
this.code = code;
|
|
7905
8388
|
}
|
|
@@ -7963,20 +8446,270 @@ class Client {
|
|
|
7963
8446
|
}
|
|
7964
8447
|
}
|
|
7965
8448
|
|
|
7966
|
-
// 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";
|
|
7967
8501
|
import fs2 from "node:fs";
|
|
8502
|
+
import net from "node:net";
|
|
7968
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";
|
|
7969
8702
|
var CONFIG_FILE = "orbito.json";
|
|
7970
8703
|
function configPath(rt, explicit) {
|
|
7971
|
-
return
|
|
8704
|
+
return path3.resolve(rt.cwd, explicit ?? CONFIG_FILE);
|
|
7972
8705
|
}
|
|
7973
8706
|
function loadConfig(rt, explicit) {
|
|
7974
8707
|
const file = configPath(rt, explicit);
|
|
7975
8708
|
let raw;
|
|
7976
8709
|
try {
|
|
7977
|
-
raw =
|
|
8710
|
+
raw = fs3.readFileSync(file, "utf8");
|
|
7978
8711
|
} catch {
|
|
7979
|
-
throw new CliError(`No ${
|
|
8712
|
+
throw new CliError(`No ${path3.basename(file)} found at ${file}. Run: orbito init --app <id>`, EXIT.invalid);
|
|
7980
8713
|
}
|
|
7981
8714
|
let json;
|
|
7982
8715
|
try {
|
|
@@ -7995,11 +8728,28 @@ ${lines.join(`
|
|
|
7995
8728
|
}
|
|
7996
8729
|
function writeConfig(rt, config, explicit) {
|
|
7997
8730
|
const file = configPath(rt, explicit);
|
|
7998
|
-
|
|
8731
|
+
fs3.writeFileSync(file, JSON.stringify(config, null, 2) + `
|
|
7999
8732
|
`);
|
|
8000
8733
|
return file;
|
|
8001
8734
|
}
|
|
8002
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
|
+
|
|
8003
8753
|
// src/cli.ts
|
|
8004
8754
|
var VERSION = package_default.version;
|
|
8005
8755
|
var DEPLOY_SCRIPTS = {
|
|
@@ -8011,7 +8761,24 @@ var DEPLOY_SCRIPTS = {
|
|
|
8011
8761
|
async function run(argv, rt) {
|
|
8012
8762
|
const program = new Command2;
|
|
8013
8763
|
let exit = EXIT.ok;
|
|
8014
|
-
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
|
+
`);
|
|
8015
8782
|
const g = () => program.opts();
|
|
8016
8783
|
const emit = (v) => rt.out(JSON.stringify(v) + `
|
|
8017
8784
|
`);
|
|
@@ -8019,11 +8786,32 @@ async function run(argv, rt) {
|
|
|
8019
8786
|
`);
|
|
8020
8787
|
const say = (s) => g().json ? info(s) : rt.out(s + `
|
|
8021
8788
|
`);
|
|
8022
|
-
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
|
+
};
|
|
8023
8811
|
const clientFor = (server, needToken = true) => {
|
|
8024
8812
|
const token = loadToken(rt, server);
|
|
8025
8813
|
if (needToken && !token)
|
|
8026
|
-
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);
|
|
8027
8815
|
return new Client(rt, server, token);
|
|
8028
8816
|
};
|
|
8029
8817
|
const authed = () => {
|
|
@@ -8032,14 +8820,39 @@ async function run(argv, rt) {
|
|
|
8032
8820
|
};
|
|
8033
8821
|
async function checkServer(api) {
|
|
8034
8822
|
const caps = Capabilities.safeParse(await api.call("GET", "/capabilities"));
|
|
8035
|
-
if (!caps.success
|
|
8823
|
+
if (!caps.success)
|
|
8036
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);
|
|
8037
8829
|
return caps.data;
|
|
8038
8830
|
}
|
|
8039
|
-
|
|
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
|
+
}
|
|
8852
|
+
async function loginTo(serverUrl) {
|
|
8040
8853
|
let server;
|
|
8041
8854
|
try {
|
|
8042
|
-
server = new URL(
|
|
8855
|
+
server = new URL(serverUrl);
|
|
8043
8856
|
} catch {
|
|
8044
8857
|
throw new CliError("--server must be a URL", EXIT.invalid);
|
|
8045
8858
|
}
|
|
@@ -8048,7 +8861,10 @@ async function run(argv, rt) {
|
|
|
8048
8861
|
try {
|
|
8049
8862
|
return await rt.fetch(auth(p), {
|
|
8050
8863
|
method: "POST",
|
|
8051
|
-
headers: {
|
|
8864
|
+
headers: {
|
|
8865
|
+
"content-type": "application/json",
|
|
8866
|
+
origin: server.origin
|
|
8867
|
+
},
|
|
8052
8868
|
body: JSON.stringify(body)
|
|
8053
8869
|
});
|
|
8054
8870
|
} catch (e) {
|
|
@@ -8062,43 +8878,60 @@ async function run(argv, rt) {
|
|
|
8062
8878
|
if (!start.ok)
|
|
8063
8879
|
throw new CliError(`The server refused the login (${start.status}).`, EXIT.denied);
|
|
8064
8880
|
const d = await start.json();
|
|
8065
|
-
|
|
8881
|
+
const link = d.verification_uri_complete ?? d.verification_uri;
|
|
8882
|
+
info(`Open ${link} and check that the code is ${d.user_code}.`);
|
|
8066
8883
|
info("If you are asked to sign in, sign in and open the link again.");
|
|
8067
|
-
let
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
8073
|
-
device_code: d.device_code,
|
|
8074
|
-
client_id: CLI_CLIENT_ID
|
|
8075
|
-
});
|
|
8076
|
-
const body = await t.json().catch(() => ({}));
|
|
8077
|
-
if (t.ok && body.access_token) {
|
|
8078
|
-
const ex = await new Client(rt, server.origin, null).call("POST", "/auth/exchange", {
|
|
8079
|
-
sessionToken: body.access_token,
|
|
8080
|
-
label: `orbito cli on ${rt.hostname}`
|
|
8081
|
-
});
|
|
8082
|
-
saveCredential(rt, server.origin, { token: ex.token, expiresAt: ex.expiresAt });
|
|
8083
|
-
say(`Logged in to ${server.origin}. Apps: ${ex.apps.map((a) => `${a.id} (${a.level})`).join(", ") || "none yet"}`);
|
|
8084
|
-
if (g().json)
|
|
8085
|
-
emit({ type: "login", server: server.origin, apps: ex.apps });
|
|
8086
|
-
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));
|
|
8087
8889
|
}
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
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);
|
|
8093
8928
|
}
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
break;
|
|
8098
|
-
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();
|
|
8099
8932
|
}
|
|
8100
|
-
|
|
8101
|
-
|
|
8933
|
+
}
|
|
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)));
|
|
8102
8935
|
program.command("logout").description("revoke and remove the machine credential").option("--server <url>", "defaults to the server in orbito.json").action(async (o) => {
|
|
8103
8936
|
const server = o.server ?? cfg().server;
|
|
8104
8937
|
const token = loadToken(rt, server);
|
|
@@ -8113,57 +8946,130 @@ async function run(argv, rt) {
|
|
|
8113
8946
|
forgetCredential(rt, server);
|
|
8114
8947
|
} catch {}
|
|
8115
8948
|
say("Logged out.");
|
|
8949
|
+
if (g().json)
|
|
8950
|
+
emit({ type: "logout", server });
|
|
8116
8951
|
});
|
|
8117
|
-
program.command("whoami").description("show server,
|
|
8118
|
-
|
|
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
|
+
}
|
|
8119
8963
|
const me = await api.call("GET", "/me");
|
|
8120
8964
|
if (g().json)
|
|
8121
8965
|
return emit({ server: c.server, ...me });
|
|
8122
|
-
|
|
8123
|
-
|
|
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;
|
|
8124
8970
|
for (const a of me.apps)
|
|
8125
|
-
|
|
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.")}
|
|
8126
9009
|
`);
|
|
8127
|
-
});
|
|
8128
|
-
program.command("init").description("validate access and create orbito.json").requiredOption("--app <id>", "the app id shown in the Orbito dashboard").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) => {
|
|
8129
|
-
let server = o.server;
|
|
8130
|
-
if (!server) {
|
|
8131
|
-
try {
|
|
8132
|
-
server = loadConfig(rt, g().config).server;
|
|
8133
|
-
} catch {
|
|
8134
|
-
throw new CliError("Pass --server <url> the first time.", EXIT.invalid);
|
|
8135
|
-
}
|
|
8136
9010
|
}
|
|
8137
|
-
|
|
8138
|
-
|
|
8139
|
-
|
|
9011
|
+
rt.out(`
|
|
9012
|
+
`);
|
|
9013
|
+
});
|
|
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) => {
|
|
9015
|
+
const server = await resolveServer(o.server);
|
|
9016
|
+
if (!loadToken(rt, server)) {
|
|
9017
|
+
info(`Not logged in to ${server} yet, logging in first.`);
|
|
9018
|
+
await loginTo(server);
|
|
9019
|
+
}
|
|
9020
|
+
const me = await clientFor(server).call("GET", "/me");
|
|
9021
|
+
let app = o.app;
|
|
9022
|
+
if (!app) {
|
|
9023
|
+
if (me.apps.length === 0)
|
|
9024
|
+
throw new CliError("You have no apps on this server yet. Ask an administrator for access.", EXIT.denied);
|
|
9025
|
+
if (me.apps.length === 1)
|
|
9026
|
+
app = me.apps[0].id;
|
|
9027
|
+
else {
|
|
9028
|
+
const list = me.apps.map((a, i) => ` ${i + 1}) ${a.id} (${a.level}) ${a.name}`).join(`
|
|
9029
|
+
`);
|
|
9030
|
+
if (!rt.isTTY)
|
|
9031
|
+
throw new CliError(`Pass --app <id>. Apps you can use:
|
|
9032
|
+
${list}`, EXIT.invalid);
|
|
9033
|
+
info(`Which app is this repository?
|
|
9034
|
+
${list}`);
|
|
9035
|
+
const n = Number((await rt.ask("Number: ")).trim());
|
|
9036
|
+
if (!Number.isInteger(n) || n < 1 || n > me.apps.length)
|
|
9037
|
+
throw new CliError("Cancelled.", EXIT.invalid);
|
|
9038
|
+
app = me.apps[n - 1].id;
|
|
9039
|
+
}
|
|
9040
|
+
}
|
|
9041
|
+
const mine = me.apps.find((a) => a.id === app);
|
|
8140
9042
|
if (!mine)
|
|
8141
|
-
throw new CliError(`You have no access to an app named '${
|
|
9043
|
+
throw new CliError(`You have no access to an app named '${app}'. Apps you can use: ${me.apps.map((a) => a.id).join(", ") || "none"}. If an administrator just gave you access, run: orbito login --server ${server}`, EXIT.denied);
|
|
8142
9044
|
const file = writeConfig(rt, {
|
|
8143
|
-
$schema: "./node_modules/@orbito/
|
|
9045
|
+
$schema: "./node_modules/@deaduchiha/orbito/config.schema.json",
|
|
8144
9046
|
version: 1,
|
|
8145
|
-
server
|
|
8146
|
-
appId:
|
|
9047
|
+
server,
|
|
9048
|
+
appId: app
|
|
8147
9049
|
}, g().config);
|
|
8148
|
-
say(`
|
|
8149
|
-
const pkgFile =
|
|
8150
|
-
if (
|
|
8151
|
-
const pkg = JSON.parse(
|
|
9050
|
+
say(`Linked to '${app}' on ${server} (you have '${mine.level}'). Wrote ${file}; commit it, credentials stay outside the repository.`);
|
|
9051
|
+
const pkgFile = path4.join(path4.dirname(file), "package.json");
|
|
9052
|
+
if (fs4.existsSync(pkgFile)) {
|
|
9053
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgFile, "utf8"));
|
|
8152
9054
|
const add = Object.entries(DEPLOY_SCRIPTS).filter(([k]) => !pkg.scripts?.[k]);
|
|
8153
9055
|
if (add.length > 0) {
|
|
8154
9056
|
say(`package.json scripts to add:
|
|
8155
9057
|
${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
8156
9058
|
`)}`);
|
|
8157
|
-
const yes = o.scripts || rt.isTTY && await rt.
|
|
9059
|
+
const yes = o.scripts || rt.isTTY && !/^n/i.test((await rt.ask("Add them? [Y/n] ")).trim());
|
|
8158
9060
|
if (yes) {
|
|
8159
9061
|
pkg.scripts = { ...pkg.scripts, ...Object.fromEntries(add) };
|
|
8160
|
-
|
|
9062
|
+
fs4.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + `
|
|
8161
9063
|
`);
|
|
8162
9064
|
say("Added.");
|
|
8163
9065
|
} else
|
|
8164
9066
|
say("Not added. Pass --scripts to add them.");
|
|
8165
9067
|
}
|
|
8166
9068
|
}
|
|
9069
|
+
say(`
|
|
9070
|
+
Next: orbito doctor then orbito deploy --dry-run`);
|
|
9071
|
+
if (g().json)
|
|
9072
|
+
emit({ type: "init", server, appId: app, level: mine.level, file });
|
|
8167
9073
|
});
|
|
8168
9074
|
const configCmd = program.command("config").description("configuration commands");
|
|
8169
9075
|
configCmd.command("validate").description("validate orbito.json and server compatibility").action(async () => {
|
|
@@ -8173,6 +9079,34 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8173
9079
|
if (g().json)
|
|
8174
9080
|
emit({ ok: true, server: c.server, appId: c.appId, api: caps.api });
|
|
8175
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
|
+
});
|
|
8176
9110
|
program.command("status").description("deployed commit, health and active job").action(async () => {
|
|
8177
9111
|
const { c, api } = authed();
|
|
8178
9112
|
const s = await api.call("GET", `/apps/${c.appId}/status`, undefined, AppStatus);
|
|
@@ -8231,12 +9165,14 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8231
9165
|
if (g().json)
|
|
8232
9166
|
emit({ type: "result", ...d });
|
|
8233
9167
|
else
|
|
8234
|
-
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 ?? ""}`);
|
|
8235
9169
|
return d.status === "deployed" ? EXIT.ok : EXIT.failed;
|
|
8236
9170
|
}
|
|
8237
|
-
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) => {
|
|
8238
9172
|
const { c, api } = authed();
|
|
8239
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);
|
|
8240
9176
|
const plan = await api.call("POST", `/apps/${c.appId}/deploy/plan`, {}, DeployPlan);
|
|
8241
9177
|
if (g().json)
|
|
8242
9178
|
emit({ type: "plan", ...plan });
|
|
@@ -8248,7 +9184,10 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8248
9184
|
if (plan.recipe)
|
|
8249
9185
|
rt.out(` recipe install ${plan.recipe.install}, build ${plan.recipe.build}, migrate ${plan.recipe.migrate}, restart ${plan.recipe.restart}
|
|
8250
9186
|
`);
|
|
8251
|
-
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.
|
|
8252
9191
|
`);
|
|
8253
9192
|
for (const b of plan.blockers)
|
|
8254
9193
|
rt.out(` BLOCKED ${b}
|
|
@@ -8265,19 +9204,15 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8265
9204
|
return;
|
|
8266
9205
|
if (!plan.sha)
|
|
8267
9206
|
throw new CliError("The commit could not be resolved.", EXIT.failed);
|
|
8268
|
-
if (plan.upToDate)
|
|
9207
|
+
if (plan.upToDate && !o.envOnly)
|
|
8269
9208
|
return say("Already up to date.");
|
|
8270
|
-
|
|
8271
|
-
if (!rt.isTTY)
|
|
8272
|
-
throw new CliError("Not a terminal: pass --yes to accept the plan.", EXIT.invalid);
|
|
8273
|
-
if (!await rt.confirm("Deploy this?"))
|
|
8274
|
-
throw new CliError("Cancelled.", EXIT.invalid);
|
|
8275
|
-
}
|
|
9209
|
+
await confirmOr("Deploy this?", o.yes);
|
|
8276
9210
|
const body = {
|
|
8277
9211
|
sha: plan.sha,
|
|
8278
9212
|
recipeHash: plan.recipeHash,
|
|
8279
|
-
idempotencyKey:
|
|
8280
|
-
...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 } : {}
|
|
8281
9216
|
};
|
|
8282
9217
|
let sub = null;
|
|
8283
9218
|
for (let i = 0;!sub; i++) {
|
|
@@ -8302,6 +9237,423 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8302
9237
|
}
|
|
8303
9238
|
exit = await finish(api, sub.deploymentId);
|
|
8304
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
|
+
});
|
|
8305
9657
|
const dep = program.command("deployments").description("deployment history");
|
|
8306
9658
|
dep.command("list").description("recent deployments").option("--limit <n>", "how many", "10").action(async (o) => {
|
|
8307
9659
|
const { c, api } = authed();
|
|
@@ -8312,7 +9664,7 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8312
9664
|
rt.out(`${d.id} ${d.startedAt} ${d.kind} ${d.status} ${d.sha?.slice(0, 12) ?? "-"} ${d.actor}
|
|
8313
9665
|
`);
|
|
8314
9666
|
});
|
|
8315
|
-
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) => {
|
|
8316
9668
|
const { c, api } = authed();
|
|
8317
9669
|
if (o.deployment) {
|
|
8318
9670
|
if (await follow(api, o.deployment) === "interrupted")
|
|
@@ -8385,19 +9737,129 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8385
9737
|
`);
|
|
8386
9738
|
for (const r of s.recent)
|
|
8387
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}
|
|
8388
9836
|
`);
|
|
8389
9837
|
});
|
|
8390
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 () => {
|
|
8391
9839
|
const { c, api } = authed();
|
|
8392
9840
|
await api.call("POST", `/apps/${c.appId}/db/migrate`, {});
|
|
8393
9841
|
});
|
|
8394
|
-
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) => {
|
|
8395
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);
|
|
8396
9855
|
const sub = await api.call("POST", `/apps/${c.appId}/rollback`, {
|
|
8397
|
-
idempotencyKey:
|
|
9856
|
+
idempotencyKey: crypto2.randomBytes(12).toString("hex"),
|
|
8398
9857
|
deploymentId: o.deployment
|
|
8399
9858
|
});
|
|
8400
|
-
|
|
9859
|
+
if (g().json)
|
|
9860
|
+
emit({ type: "submitted", ...sub });
|
|
9861
|
+
else
|
|
9862
|
+
say(`Submitted ${sub.deploymentId}.`);
|
|
8401
9863
|
if (o.detach)
|
|
8402
9864
|
return;
|
|
8403
9865
|
if (await follow(api, sub.deploymentId) === "interrupted")
|
|
@@ -8411,7 +9873,11 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8411
9873
|
try {
|
|
8412
9874
|
results.push({ check: name, ok: true, detail: await fn() });
|
|
8413
9875
|
} catch (e) {
|
|
8414
|
-
results.push({
|
|
9876
|
+
results.push({
|
|
9877
|
+
check: name,
|
|
9878
|
+
ok: false,
|
|
9879
|
+
detail: e.message
|
|
9880
|
+
});
|
|
8415
9881
|
}
|
|
8416
9882
|
};
|
|
8417
9883
|
let c = null;
|
|
@@ -8455,7 +9921,11 @@ ${add.map(([k, v]) => ` "${k}": "${v}"`).join(`
|
|
|
8455
9921
|
return e.exitCode === 0 ? EXIT.ok : EXIT.invalid;
|
|
8456
9922
|
const err = e;
|
|
8457
9923
|
if (g().json)
|
|
8458
|
-
emit({
|
|
9924
|
+
emit({
|
|
9925
|
+
type: "error",
|
|
9926
|
+
error: err.message,
|
|
9927
|
+
code: e instanceof ApiFailure ? e.code : "error"
|
|
9928
|
+
});
|
|
8459
9929
|
else
|
|
8460
9930
|
info(err.message);
|
|
8461
9931
|
return e instanceof CliError ? e.exit : EXIT.failed;
|
|
@@ -8498,5 +9968,55 @@ async function readSse(res, signal, on) {
|
|
|
8498
9968
|
}
|
|
8499
9969
|
}
|
|
8500
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
|
+
|
|
8501
10013
|
// src/main.ts
|
|
8502
|
-
|
|
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
|
+
`);
|