@divergeai/cli 0.1.14 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/dist/diverge.mjs +872 -639
- package/package.json +1 -1
package/dist/diverge.mjs
CHANGED
|
@@ -114,17 +114,17 @@ var require_visit = __commonJS({
|
|
|
114
114
|
visit.BREAK = BREAK;
|
|
115
115
|
visit.SKIP = SKIP;
|
|
116
116
|
visit.REMOVE = REMOVE;
|
|
117
|
-
function visit_(key, node, visitor,
|
|
118
|
-
const ctrl = callVisitor(key, node, visitor,
|
|
117
|
+
function visit_(key, node, visitor, path15) {
|
|
118
|
+
const ctrl = callVisitor(key, node, visitor, path15);
|
|
119
119
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
120
|
-
replaceNode(key,
|
|
121
|
-
return visit_(key, ctrl, visitor,
|
|
120
|
+
replaceNode(key, path15, ctrl);
|
|
121
|
+
return visit_(key, ctrl, visitor, path15);
|
|
122
122
|
}
|
|
123
123
|
if (typeof ctrl !== "symbol") {
|
|
124
124
|
if (identity.isCollection(node)) {
|
|
125
|
-
|
|
125
|
+
path15 = Object.freeze(path15.concat(node));
|
|
126
126
|
for (let i = 0; i < node.items.length; ++i) {
|
|
127
|
-
const ci = visit_(i, node.items[i], visitor,
|
|
127
|
+
const ci = visit_(i, node.items[i], visitor, path15);
|
|
128
128
|
if (typeof ci === "number")
|
|
129
129
|
i = ci - 1;
|
|
130
130
|
else if (ci === BREAK)
|
|
@@ -135,13 +135,13 @@ var require_visit = __commonJS({
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
} else if (identity.isPair(node)) {
|
|
138
|
-
|
|
139
|
-
const ck = visit_("key", node.key, visitor,
|
|
138
|
+
path15 = Object.freeze(path15.concat(node));
|
|
139
|
+
const ck = visit_("key", node.key, visitor, path15);
|
|
140
140
|
if (ck === BREAK)
|
|
141
141
|
return BREAK;
|
|
142
142
|
else if (ck === REMOVE)
|
|
143
143
|
node.key = null;
|
|
144
|
-
const cv = visit_("value", node.value, visitor,
|
|
144
|
+
const cv = visit_("value", node.value, visitor, path15);
|
|
145
145
|
if (cv === BREAK)
|
|
146
146
|
return BREAK;
|
|
147
147
|
else if (cv === REMOVE)
|
|
@@ -162,17 +162,17 @@ var require_visit = __commonJS({
|
|
|
162
162
|
visitAsync.BREAK = BREAK;
|
|
163
163
|
visitAsync.SKIP = SKIP;
|
|
164
164
|
visitAsync.REMOVE = REMOVE;
|
|
165
|
-
async function visitAsync_(key, node, visitor,
|
|
166
|
-
const ctrl = await callVisitor(key, node, visitor,
|
|
165
|
+
async function visitAsync_(key, node, visitor, path15) {
|
|
166
|
+
const ctrl = await callVisitor(key, node, visitor, path15);
|
|
167
167
|
if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
|
|
168
|
-
replaceNode(key,
|
|
169
|
-
return visitAsync_(key, ctrl, visitor,
|
|
168
|
+
replaceNode(key, path15, ctrl);
|
|
169
|
+
return visitAsync_(key, ctrl, visitor, path15);
|
|
170
170
|
}
|
|
171
171
|
if (typeof ctrl !== "symbol") {
|
|
172
172
|
if (identity.isCollection(node)) {
|
|
173
|
-
|
|
173
|
+
path15 = Object.freeze(path15.concat(node));
|
|
174
174
|
for (let i = 0; i < node.items.length; ++i) {
|
|
175
|
-
const ci = await visitAsync_(i, node.items[i], visitor,
|
|
175
|
+
const ci = await visitAsync_(i, node.items[i], visitor, path15);
|
|
176
176
|
if (typeof ci === "number")
|
|
177
177
|
i = ci - 1;
|
|
178
178
|
else if (ci === BREAK)
|
|
@@ -183,13 +183,13 @@ var require_visit = __commonJS({
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
} else if (identity.isPair(node)) {
|
|
186
|
-
|
|
187
|
-
const ck = await visitAsync_("key", node.key, visitor,
|
|
186
|
+
path15 = Object.freeze(path15.concat(node));
|
|
187
|
+
const ck = await visitAsync_("key", node.key, visitor, path15);
|
|
188
188
|
if (ck === BREAK)
|
|
189
189
|
return BREAK;
|
|
190
190
|
else if (ck === REMOVE)
|
|
191
191
|
node.key = null;
|
|
192
|
-
const cv = await visitAsync_("value", node.value, visitor,
|
|
192
|
+
const cv = await visitAsync_("value", node.value, visitor, path15);
|
|
193
193
|
if (cv === BREAK)
|
|
194
194
|
return BREAK;
|
|
195
195
|
else if (cv === REMOVE)
|
|
@@ -216,23 +216,23 @@ var require_visit = __commonJS({
|
|
|
216
216
|
}
|
|
217
217
|
return visitor;
|
|
218
218
|
}
|
|
219
|
-
function callVisitor(key, node, visitor,
|
|
219
|
+
function callVisitor(key, node, visitor, path15) {
|
|
220
220
|
if (typeof visitor === "function")
|
|
221
|
-
return visitor(key, node,
|
|
221
|
+
return visitor(key, node, path15);
|
|
222
222
|
if (identity.isMap(node))
|
|
223
|
-
return visitor.Map?.(key, node,
|
|
223
|
+
return visitor.Map?.(key, node, path15);
|
|
224
224
|
if (identity.isSeq(node))
|
|
225
|
-
return visitor.Seq?.(key, node,
|
|
225
|
+
return visitor.Seq?.(key, node, path15);
|
|
226
226
|
if (identity.isPair(node))
|
|
227
|
-
return visitor.Pair?.(key, node,
|
|
227
|
+
return visitor.Pair?.(key, node, path15);
|
|
228
228
|
if (identity.isScalar(node))
|
|
229
|
-
return visitor.Scalar?.(key, node,
|
|
229
|
+
return visitor.Scalar?.(key, node, path15);
|
|
230
230
|
if (identity.isAlias(node))
|
|
231
|
-
return visitor.Alias?.(key, node,
|
|
231
|
+
return visitor.Alias?.(key, node, path15);
|
|
232
232
|
return void 0;
|
|
233
233
|
}
|
|
234
|
-
function replaceNode(key,
|
|
235
|
-
const parent =
|
|
234
|
+
function replaceNode(key, path15, node) {
|
|
235
|
+
const parent = path15[path15.length - 1];
|
|
236
236
|
if (identity.isCollection(parent)) {
|
|
237
237
|
parent.items[key] = node;
|
|
238
238
|
} else if (identity.isPair(parent)) {
|
|
@@ -842,10 +842,10 @@ var require_Collection = __commonJS({
|
|
|
842
842
|
var createNode = require_createNode();
|
|
843
843
|
var identity = require_identity();
|
|
844
844
|
var Node = require_Node();
|
|
845
|
-
function collectionFromPath(schema,
|
|
845
|
+
function collectionFromPath(schema, path15, value) {
|
|
846
846
|
let v = value;
|
|
847
|
-
for (let i =
|
|
848
|
-
const k =
|
|
847
|
+
for (let i = path15.length - 1; i >= 0; --i) {
|
|
848
|
+
const k = path15[i];
|
|
849
849
|
if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
|
|
850
850
|
const a = [];
|
|
851
851
|
a[k] = v;
|
|
@@ -864,7 +864,7 @@ var require_Collection = __commonJS({
|
|
|
864
864
|
sourceObjects: /* @__PURE__ */ new Map()
|
|
865
865
|
});
|
|
866
866
|
}
|
|
867
|
-
var isEmptyPath = (
|
|
867
|
+
var isEmptyPath = (path15) => path15 == null || typeof path15 === "object" && !!path15[Symbol.iterator]().next().done;
|
|
868
868
|
var Collection = class extends Node.NodeBase {
|
|
869
869
|
constructor(type, schema) {
|
|
870
870
|
super(type);
|
|
@@ -894,11 +894,11 @@ var require_Collection = __commonJS({
|
|
|
894
894
|
* be a Pair instance or a `{ key, value }` object, which may not have a key
|
|
895
895
|
* that already exists in the map.
|
|
896
896
|
*/
|
|
897
|
-
addIn(
|
|
898
|
-
if (isEmptyPath(
|
|
897
|
+
addIn(path15, value) {
|
|
898
|
+
if (isEmptyPath(path15))
|
|
899
899
|
this.add(value);
|
|
900
900
|
else {
|
|
901
|
-
const [key, ...rest] =
|
|
901
|
+
const [key, ...rest] = path15;
|
|
902
902
|
const node = this.get(key, true);
|
|
903
903
|
if (identity.isCollection(node))
|
|
904
904
|
node.addIn(rest, value);
|
|
@@ -912,8 +912,8 @@ var require_Collection = __commonJS({
|
|
|
912
912
|
* Removes a value from the collection.
|
|
913
913
|
* @returns `true` if the item was found and removed.
|
|
914
914
|
*/
|
|
915
|
-
deleteIn(
|
|
916
|
-
const [key, ...rest] =
|
|
915
|
+
deleteIn(path15) {
|
|
916
|
+
const [key, ...rest] = path15;
|
|
917
917
|
if (rest.length === 0)
|
|
918
918
|
return this.delete(key);
|
|
919
919
|
const node = this.get(key, true);
|
|
@@ -927,8 +927,8 @@ var require_Collection = __commonJS({
|
|
|
927
927
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
928
928
|
* `true` (collections are always returned intact).
|
|
929
929
|
*/
|
|
930
|
-
getIn(
|
|
931
|
-
const [key, ...rest] =
|
|
930
|
+
getIn(path15, keepScalar) {
|
|
931
|
+
const [key, ...rest] = path15;
|
|
932
932
|
const node = this.get(key, true);
|
|
933
933
|
if (rest.length === 0)
|
|
934
934
|
return !keepScalar && identity.isScalar(node) ? node.value : node;
|
|
@@ -946,8 +946,8 @@ var require_Collection = __commonJS({
|
|
|
946
946
|
/**
|
|
947
947
|
* Checks if the collection includes a value with the key `key`.
|
|
948
948
|
*/
|
|
949
|
-
hasIn(
|
|
950
|
-
const [key, ...rest] =
|
|
949
|
+
hasIn(path15) {
|
|
950
|
+
const [key, ...rest] = path15;
|
|
951
951
|
if (rest.length === 0)
|
|
952
952
|
return this.has(key);
|
|
953
953
|
const node = this.get(key, true);
|
|
@@ -957,8 +957,8 @@ var require_Collection = __commonJS({
|
|
|
957
957
|
* Sets a value in this collection. For `!!set`, `value` needs to be a
|
|
958
958
|
* boolean to add/remove the item from the set.
|
|
959
959
|
*/
|
|
960
|
-
setIn(
|
|
961
|
-
const [key, ...rest] =
|
|
960
|
+
setIn(path15, value) {
|
|
961
|
+
const [key, ...rest] = path15;
|
|
962
962
|
if (rest.length === 0) {
|
|
963
963
|
this.set(key, value);
|
|
964
964
|
} else {
|
|
@@ -3473,9 +3473,9 @@ var require_Document = __commonJS({
|
|
|
3473
3473
|
this.contents.add(value);
|
|
3474
3474
|
}
|
|
3475
3475
|
/** Adds a value to the document. */
|
|
3476
|
-
addIn(
|
|
3476
|
+
addIn(path15, value) {
|
|
3477
3477
|
if (assertCollection(this.contents))
|
|
3478
|
-
this.contents.addIn(
|
|
3478
|
+
this.contents.addIn(path15, value);
|
|
3479
3479
|
}
|
|
3480
3480
|
/**
|
|
3481
3481
|
* Create a new `Alias` node, ensuring that the target `node` has the required anchor.
|
|
@@ -3550,14 +3550,14 @@ var require_Document = __commonJS({
|
|
|
3550
3550
|
* Removes a value from the document.
|
|
3551
3551
|
* @returns `true` if the item was found and removed.
|
|
3552
3552
|
*/
|
|
3553
|
-
deleteIn(
|
|
3554
|
-
if (Collection.isEmptyPath(
|
|
3553
|
+
deleteIn(path15) {
|
|
3554
|
+
if (Collection.isEmptyPath(path15)) {
|
|
3555
3555
|
if (this.contents == null)
|
|
3556
3556
|
return false;
|
|
3557
3557
|
this.contents = null;
|
|
3558
3558
|
return true;
|
|
3559
3559
|
}
|
|
3560
|
-
return assertCollection(this.contents) ? this.contents.deleteIn(
|
|
3560
|
+
return assertCollection(this.contents) ? this.contents.deleteIn(path15) : false;
|
|
3561
3561
|
}
|
|
3562
3562
|
/**
|
|
3563
3563
|
* Returns item at `key`, or `undefined` if not found. By default unwraps
|
|
@@ -3572,10 +3572,10 @@ var require_Document = __commonJS({
|
|
|
3572
3572
|
* scalar values from their surrounding node; to disable set `keepScalar` to
|
|
3573
3573
|
* `true` (collections are always returned intact).
|
|
3574
3574
|
*/
|
|
3575
|
-
getIn(
|
|
3576
|
-
if (Collection.isEmptyPath(
|
|
3575
|
+
getIn(path15, keepScalar) {
|
|
3576
|
+
if (Collection.isEmptyPath(path15))
|
|
3577
3577
|
return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
|
|
3578
|
-
return identity.isCollection(this.contents) ? this.contents.getIn(
|
|
3578
|
+
return identity.isCollection(this.contents) ? this.contents.getIn(path15, keepScalar) : void 0;
|
|
3579
3579
|
}
|
|
3580
3580
|
/**
|
|
3581
3581
|
* Checks if the document includes a value with the key `key`.
|
|
@@ -3586,10 +3586,10 @@ var require_Document = __commonJS({
|
|
|
3586
3586
|
/**
|
|
3587
3587
|
* Checks if the document includes a value at `path`.
|
|
3588
3588
|
*/
|
|
3589
|
-
hasIn(
|
|
3590
|
-
if (Collection.isEmptyPath(
|
|
3589
|
+
hasIn(path15) {
|
|
3590
|
+
if (Collection.isEmptyPath(path15))
|
|
3591
3591
|
return this.contents !== void 0;
|
|
3592
|
-
return identity.isCollection(this.contents) ? this.contents.hasIn(
|
|
3592
|
+
return identity.isCollection(this.contents) ? this.contents.hasIn(path15) : false;
|
|
3593
3593
|
}
|
|
3594
3594
|
/**
|
|
3595
3595
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
@@ -3606,13 +3606,13 @@ var require_Document = __commonJS({
|
|
|
3606
3606
|
* Sets a value in this document. For `!!set`, `value` needs to be a
|
|
3607
3607
|
* boolean to add/remove the item from the set.
|
|
3608
3608
|
*/
|
|
3609
|
-
setIn(
|
|
3610
|
-
if (Collection.isEmptyPath(
|
|
3609
|
+
setIn(path15, value) {
|
|
3610
|
+
if (Collection.isEmptyPath(path15)) {
|
|
3611
3611
|
this.contents = value;
|
|
3612
3612
|
} else if (this.contents == null) {
|
|
3613
|
-
this.contents = Collection.collectionFromPath(this.schema, Array.from(
|
|
3613
|
+
this.contents = Collection.collectionFromPath(this.schema, Array.from(path15), value);
|
|
3614
3614
|
} else if (assertCollection(this.contents)) {
|
|
3615
|
-
this.contents.setIn(
|
|
3615
|
+
this.contents.setIn(path15, value);
|
|
3616
3616
|
}
|
|
3617
3617
|
}
|
|
3618
3618
|
/**
|
|
@@ -5572,9 +5572,9 @@ var require_cst_visit = __commonJS({
|
|
|
5572
5572
|
visit.BREAK = BREAK;
|
|
5573
5573
|
visit.SKIP = SKIP;
|
|
5574
5574
|
visit.REMOVE = REMOVE;
|
|
5575
|
-
visit.itemAtPath = (cst,
|
|
5575
|
+
visit.itemAtPath = (cst, path15) => {
|
|
5576
5576
|
let item = cst;
|
|
5577
|
-
for (const [field, index] of
|
|
5577
|
+
for (const [field, index] of path15) {
|
|
5578
5578
|
const tok = item?.[field];
|
|
5579
5579
|
if (tok && "items" in tok) {
|
|
5580
5580
|
item = tok.items[index];
|
|
@@ -5583,23 +5583,23 @@ var require_cst_visit = __commonJS({
|
|
|
5583
5583
|
}
|
|
5584
5584
|
return item;
|
|
5585
5585
|
};
|
|
5586
|
-
visit.parentCollection = (cst,
|
|
5587
|
-
const parent = visit.itemAtPath(cst,
|
|
5588
|
-
const field =
|
|
5586
|
+
visit.parentCollection = (cst, path15) => {
|
|
5587
|
+
const parent = visit.itemAtPath(cst, path15.slice(0, -1));
|
|
5588
|
+
const field = path15[path15.length - 1][0];
|
|
5589
5589
|
const coll = parent?.[field];
|
|
5590
5590
|
if (coll && "items" in coll)
|
|
5591
5591
|
return coll;
|
|
5592
5592
|
throw new Error("Parent collection not found");
|
|
5593
5593
|
};
|
|
5594
|
-
function _visit(
|
|
5595
|
-
let ctrl = visitor(item,
|
|
5594
|
+
function _visit(path15, item, visitor) {
|
|
5595
|
+
let ctrl = visitor(item, path15);
|
|
5596
5596
|
if (typeof ctrl === "symbol")
|
|
5597
5597
|
return ctrl;
|
|
5598
5598
|
for (const field of ["key", "value"]) {
|
|
5599
5599
|
const token = item[field];
|
|
5600
5600
|
if (token && "items" in token) {
|
|
5601
5601
|
for (let i = 0; i < token.items.length; ++i) {
|
|
5602
|
-
const ci = _visit(Object.freeze(
|
|
5602
|
+
const ci = _visit(Object.freeze(path15.concat([[field, i]])), token.items[i], visitor);
|
|
5603
5603
|
if (typeof ci === "number")
|
|
5604
5604
|
i = ci - 1;
|
|
5605
5605
|
else if (ci === BREAK)
|
|
@@ -5610,10 +5610,10 @@ var require_cst_visit = __commonJS({
|
|
|
5610
5610
|
}
|
|
5611
5611
|
}
|
|
5612
5612
|
if (typeof ctrl === "function" && field === "key")
|
|
5613
|
-
ctrl = ctrl(item,
|
|
5613
|
+
ctrl = ctrl(item, path15);
|
|
5614
5614
|
}
|
|
5615
5615
|
}
|
|
5616
|
-
return typeof ctrl === "function" ? ctrl(item,
|
|
5616
|
+
return typeof ctrl === "function" ? ctrl(item, path15) : ctrl;
|
|
5617
5617
|
}
|
|
5618
5618
|
exports.visit = visit;
|
|
5619
5619
|
}
|
|
@@ -7370,7 +7370,8 @@ var ExitCode = {
|
|
|
7370
7370
|
Failure: 1,
|
|
7371
7371
|
Pending: 3,
|
|
7372
7372
|
Rejected: 4,
|
|
7373
|
-
Expired: 5
|
|
7373
|
+
Expired: 5,
|
|
7374
|
+
LoginRequired: 6
|
|
7374
7375
|
};
|
|
7375
7376
|
var CliError = class extends Error {
|
|
7376
7377
|
constructor(message, exitCode = ExitCode.Failure) {
|
|
@@ -7383,11 +7384,46 @@ var CliError = class extends Error {
|
|
|
7383
7384
|
|
|
7384
7385
|
// src/commands/agents.ts
|
|
7385
7386
|
import fs8 from "node:fs";
|
|
7386
|
-
import
|
|
7387
|
-
import { parseArgs as
|
|
7387
|
+
import path12 from "node:path";
|
|
7388
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
7389
|
+
|
|
7390
|
+
// src/auth/oauth-client.ts
|
|
7391
|
+
import fs3 from "node:fs";
|
|
7392
|
+
import path3 from "node:path";
|
|
7393
|
+
|
|
7394
|
+
// src/config.ts
|
|
7395
|
+
import os from "node:os";
|
|
7396
|
+
import path from "node:path";
|
|
7397
|
+
var DEFAULT_API_URL = "https://divergeai.net";
|
|
7398
|
+
var DEFAULT_CLIENT_ID = "diverge";
|
|
7399
|
+
function configDir(env = process.env) {
|
|
7400
|
+
return env.DIVERGE_HOME?.trim() || path.join(os.homedir(), ".diverge");
|
|
7401
|
+
}
|
|
7402
|
+
function resolveApiUrl(flag, env = process.env) {
|
|
7403
|
+
const chosen = flag?.trim() || env.DIVERGENT_API_URL?.trim() || DEFAULT_API_URL;
|
|
7404
|
+
return chosen.replace(/\/+$/, "");
|
|
7405
|
+
}
|
|
7406
|
+
function clientId(env = process.env) {
|
|
7407
|
+
return env.DIVERGENT_DIVERGE_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID;
|
|
7408
|
+
}
|
|
7409
|
+
function deploymentKey(baseUrl) {
|
|
7410
|
+
const url = new URL(baseUrl);
|
|
7411
|
+
const pathPart = url.pathname.replace(/\/+$/, "");
|
|
7412
|
+
return `${url.protocol === "http:" ? "http_" : ""}${url.host}${pathPart}`.replace(/[^A-Za-z0-9.-]/g, "_");
|
|
7413
|
+
}
|
|
7414
|
+
function tokenSource(env, baseUrl) {
|
|
7415
|
+
const normalized = baseUrl.replace(/\/+$/, "");
|
|
7416
|
+
const dir = path.join(configDir(env), "logins", deploymentKey(normalized));
|
|
7417
|
+
return {
|
|
7418
|
+
loginFile: path.join(dir, "login.json"),
|
|
7419
|
+
tokensDir: path.join(dir, "tokens"),
|
|
7420
|
+
baseUrl: normalized,
|
|
7421
|
+
clientId: clientId(env)
|
|
7422
|
+
};
|
|
7423
|
+
}
|
|
7388
7424
|
|
|
7389
7425
|
// src/version.ts
|
|
7390
|
-
var VERSION = true ? "0.1.
|
|
7426
|
+
var VERSION = true ? "0.1.23" : "dev";
|
|
7391
7427
|
var USER_AGENT = `diverge/${VERSION}`;
|
|
7392
7428
|
|
|
7393
7429
|
// src/http.ts
|
|
@@ -7397,9 +7433,13 @@ var HttpError = class extends Error {
|
|
|
7397
7433
|
this.status = status2;
|
|
7398
7434
|
this.body = body;
|
|
7399
7435
|
this.name = "HttpError";
|
|
7436
|
+
const error = body && typeof body === "object" ? body.error : void 0;
|
|
7437
|
+
this.code = typeof error === "string" ? error : void 0;
|
|
7400
7438
|
}
|
|
7401
7439
|
status;
|
|
7402
7440
|
body;
|
|
7441
|
+
/** The OAuth error code (RFC 6749 §5.2) when the body carries one: branch on it, never on the message. */
|
|
7442
|
+
code;
|
|
7403
7443
|
};
|
|
7404
7444
|
function errorMessageFrom(body, fallback) {
|
|
7405
7445
|
if (body && typeof body === "object") {
|
|
@@ -7566,7 +7606,7 @@ function buildAuthorizeUrl(params) {
|
|
|
7566
7606
|
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
7567
7607
|
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
7568
7608
|
url.searchParams.set("code_challenge_method", "S256");
|
|
7569
|
-
url.searchParams.set("scope", params.scopes);
|
|
7609
|
+
if (params.scopes) url.searchParams.set("scope", params.scopes);
|
|
7570
7610
|
url.searchParams.set("state", params.state);
|
|
7571
7611
|
if (params.organizationId) {
|
|
7572
7612
|
url.searchParams.set("organization_id", params.organizationId);
|
|
@@ -7582,11 +7622,11 @@ import fs2 from "node:fs";
|
|
|
7582
7622
|
|
|
7583
7623
|
// src/fs/atomic.ts
|
|
7584
7624
|
import fs from "node:fs";
|
|
7585
|
-
import
|
|
7625
|
+
import path2 from "node:path";
|
|
7586
7626
|
function writeTextAtomic(filePath, text, mode = 384) {
|
|
7587
|
-
const dir =
|
|
7627
|
+
const dir = path2.dirname(filePath);
|
|
7588
7628
|
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
7589
|
-
const tmp =
|
|
7629
|
+
const tmp = path2.join(dir, `.${path2.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
7590
7630
|
const fd = fs.openSync(tmp, "wx", mode);
|
|
7591
7631
|
try {
|
|
7592
7632
|
fs.writeFileSync(fd, text);
|
|
@@ -7607,6 +7647,23 @@ function writeJsonAtomic(filePath, value, mode = 384) {
|
|
|
7607
7647
|
}
|
|
7608
7648
|
|
|
7609
7649
|
// src/auth/token-cache.ts
|
|
7650
|
+
function loadLoginRecord(file) {
|
|
7651
|
+
try {
|
|
7652
|
+
const parsed = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
7653
|
+
if (typeof parsed.base_url !== "string" || typeof parsed.refresh_token !== "string") return null;
|
|
7654
|
+
return {
|
|
7655
|
+
base_url: parsed.base_url,
|
|
7656
|
+
refresh_token: parsed.refresh_token,
|
|
7657
|
+
...typeof parsed.account === "string" && parsed.account ? { account: parsed.account } : {},
|
|
7658
|
+
...typeof parsed.organization === "string" && parsed.organization ? { organization: parsed.organization } : {}
|
|
7659
|
+
};
|
|
7660
|
+
} catch {
|
|
7661
|
+
return null;
|
|
7662
|
+
}
|
|
7663
|
+
}
|
|
7664
|
+
function saveLoginRecord(file, record) {
|
|
7665
|
+
writeJsonAtomic(file, record, 384);
|
|
7666
|
+
}
|
|
7610
7667
|
function loadUserToken(file) {
|
|
7611
7668
|
let text;
|
|
7612
7669
|
try {
|
|
@@ -7619,12 +7676,7 @@ function loadUserToken(file) {
|
|
|
7619
7676
|
if (typeof parsed.access_token !== "string" || typeof parsed.base_url !== "string" || typeof parsed.expires_at !== "number") {
|
|
7620
7677
|
return null;
|
|
7621
7678
|
}
|
|
7622
|
-
return {
|
|
7623
|
-
access_token: parsed.access_token,
|
|
7624
|
-
refresh_token: typeof parsed.refresh_token === "string" ? parsed.refresh_token : null,
|
|
7625
|
-
expires_at: parsed.expires_at,
|
|
7626
|
-
base_url: parsed.base_url
|
|
7627
|
-
};
|
|
7679
|
+
return { access_token: parsed.access_token, expires_at: parsed.expires_at, base_url: parsed.base_url };
|
|
7628
7680
|
} catch {
|
|
7629
7681
|
return null;
|
|
7630
7682
|
}
|
|
@@ -7632,14 +7684,6 @@ function loadUserToken(file) {
|
|
|
7632
7684
|
function saveUserToken(file, token) {
|
|
7633
7685
|
writeJsonAtomic(file, token, 384);
|
|
7634
7686
|
}
|
|
7635
|
-
function deleteUserToken(file) {
|
|
7636
|
-
try {
|
|
7637
|
-
fs2.rmSync(file);
|
|
7638
|
-
return true;
|
|
7639
|
-
} catch {
|
|
7640
|
-
return false;
|
|
7641
|
-
}
|
|
7642
|
-
}
|
|
7643
7687
|
function isFresh(token, freshForSeconds, nowMs = Date.now()) {
|
|
7644
7688
|
return token.expires_at - nowMs / 1e3 > freshForSeconds;
|
|
7645
7689
|
}
|
|
@@ -7656,7 +7700,10 @@ function tokenClaims(accessToken) {
|
|
|
7656
7700
|
|
|
7657
7701
|
// src/auth/oauth-client.ts
|
|
7658
7702
|
var DEFAULT_EXPIRES_IN_SECONDS = 3600;
|
|
7659
|
-
|
|
7703
|
+
function loginRequired(message) {
|
|
7704
|
+
return new CliError(message, ExitCode.LoginRequired);
|
|
7705
|
+
}
|
|
7706
|
+
async function postToken(baseUrl, body, refused) {
|
|
7660
7707
|
try {
|
|
7661
7708
|
return await fetchJson(tokenEndpoint(baseUrl), {
|
|
7662
7709
|
method: "POST",
|
|
@@ -7664,14 +7711,13 @@ async function postToken(baseUrl, body) {
|
|
|
7664
7711
|
body: JSON.stringify(body)
|
|
7665
7712
|
});
|
|
7666
7713
|
} catch (error) {
|
|
7667
|
-
if (error instanceof HttpError) throw new CliError(`Token endpoint returned HTTP ${error.status}: ${error.message}`);
|
|
7668
|
-
throw error;
|
|
7714
|
+
if (error instanceof HttpError) throw refused?.(error) ?? new CliError(`Token endpoint returned HTTP ${error.status}: ${error.message}`);
|
|
7715
|
+
throw new CliError(`Cannot reach ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7669
7716
|
}
|
|
7670
7717
|
}
|
|
7671
|
-
function
|
|
7718
|
+
function accessFrom(payload, baseUrl, nowMs) {
|
|
7672
7719
|
return {
|
|
7673
7720
|
access_token: payload.access_token,
|
|
7674
|
-
refresh_token: payload.refresh_token ?? previousRefresh,
|
|
7675
7721
|
expires_at: nowMs / 1e3 + Number(payload.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS),
|
|
7676
7722
|
base_url: baseUrl.replace(/\/+$/, "")
|
|
7677
7723
|
};
|
|
@@ -7693,8 +7739,6 @@ async function login(options, deps = defaultDeps) {
|
|
|
7693
7739
|
codeChallenge: challengeS256(verifier),
|
|
7694
7740
|
scopes: options.scopes,
|
|
7695
7741
|
state,
|
|
7696
|
-
organizationId: options.organizationId,
|
|
7697
|
-
selectOrganization: options.selectOrganization,
|
|
7698
7742
|
loginHint: options.loginHint
|
|
7699
7743
|
});
|
|
7700
7744
|
const pending = loopback.waitForCode(state, options.timeoutMs);
|
|
@@ -7709,41 +7753,90 @@ async function login(options, deps = defaultDeps) {
|
|
|
7709
7753
|
redirect_uri: loopback.redirectUri,
|
|
7710
7754
|
code_verifier: verifier
|
|
7711
7755
|
});
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
return token;
|
|
7756
|
+
if (!payload.refresh_token) throw new CliError("The platform issued no refresh token for this sign-in; run: diverge login");
|
|
7757
|
+
return { refreshToken: payload.refresh_token, token: accessFrom(payload, options.baseUrl, deps.now()) };
|
|
7715
7758
|
} finally {
|
|
7716
7759
|
await loopback.close();
|
|
7717
7760
|
}
|
|
7718
7761
|
}
|
|
7719
|
-
async function
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7762
|
+
async function issueToken(source, record, organizationId, nowMs = Date.now()) {
|
|
7763
|
+
const payload = await postToken(
|
|
7764
|
+
source.baseUrl,
|
|
7765
|
+
{
|
|
7766
|
+
grant_type: "refresh_token",
|
|
7767
|
+
refresh_token: record.refresh_token,
|
|
7768
|
+
client_id: source.clientId,
|
|
7769
|
+
...organizationId ? { organization_id: organizationId } : {}
|
|
7770
|
+
},
|
|
7771
|
+
(error) => {
|
|
7772
|
+
if (error.code === "invalid_grant") return loginRequired(`Your diverge login is no longer valid (${error.message}). Run: diverge login`);
|
|
7773
|
+
if (error.code === "invalid_scope") return new CliError(`Not granted${organizationId ? ` at organization ${organizationId}` : ""}: ${error.message}`);
|
|
7774
|
+
return void 0;
|
|
7775
|
+
}
|
|
7776
|
+
);
|
|
7777
|
+
return {
|
|
7778
|
+
record: { ...record, refresh_token: payload.refresh_token ?? record.refresh_token },
|
|
7779
|
+
token: accessFrom(payload, source.baseUrl, nowMs)
|
|
7780
|
+
};
|
|
7737
7781
|
}
|
|
7738
|
-
|
|
7739
|
-
const
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
return
|
|
7782
|
+
function loadLogin(source) {
|
|
7783
|
+
const record = loadLoginRecord(source.loginFile);
|
|
7784
|
+
return record && record.base_url === source.baseUrl ? record : null;
|
|
7785
|
+
}
|
|
7786
|
+
function saveLogin(source, record) {
|
|
7787
|
+
saveLoginRecord(source.loginFile, record);
|
|
7788
|
+
}
|
|
7789
|
+
function tokenFileFor(source, organizationId) {
|
|
7790
|
+
return path3.join(source.tokensDir, `${organizationId ? organizationId.replace(/[^A-Za-z0-9._-]/g, "_") : "_platform"}.json`);
|
|
7791
|
+
}
|
|
7792
|
+
function saveOrganizationToken(source, organizationId, token) {
|
|
7793
|
+
saveUserToken(tokenFileFor(source, organizationId), token);
|
|
7794
|
+
}
|
|
7795
|
+
function saveRotated(source, presentedRefresh, next) {
|
|
7796
|
+
const current = loadLoginRecord(source.loginFile);
|
|
7797
|
+
if (current && current.refresh_token !== presentedRefresh) return;
|
|
7798
|
+
saveLogin(source, next);
|
|
7799
|
+
}
|
|
7800
|
+
function forgetLogin(source) {
|
|
7801
|
+
fs3.rmSync(path3.dirname(source.loginFile), { recursive: true, force: true });
|
|
7802
|
+
}
|
|
7803
|
+
function noLogin(baseUrl, alternative) {
|
|
7804
|
+
const command = `diverge login${baseUrl === DEFAULT_API_URL ? "" : ` --api-url ${baseUrl}`}`;
|
|
7805
|
+
return loginRequired(`No cached login for ${baseUrl}. Run: ${command}${alternative ? `
|
|
7806
|
+
${alternative}` : ""}`);
|
|
7807
|
+
}
|
|
7808
|
+
function requireLogin(source) {
|
|
7809
|
+
const record = loadLogin(source);
|
|
7810
|
+
if (!record) throw noLogin(source.baseUrl);
|
|
7811
|
+
return record;
|
|
7812
|
+
}
|
|
7813
|
+
async function useOrganization(source, organizationId, nowMs = Date.now()) {
|
|
7814
|
+
const token = await getUserToken(source, { organizationId, freshForSeconds: 0, nowMs });
|
|
7815
|
+
const record = requireLogin(source);
|
|
7816
|
+
saveLogin(source, { ...record, organization: organizationId });
|
|
7817
|
+
return token;
|
|
7818
|
+
}
|
|
7819
|
+
async function getUserToken(source, options = {}) {
|
|
7820
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
7821
|
+
const record = requireLogin(source);
|
|
7822
|
+
const organizationId = options.organizationId?.trim() || record.organization;
|
|
7823
|
+
const file = tokenFileFor(source, organizationId);
|
|
7824
|
+
const cached = loadUserToken(file);
|
|
7825
|
+
if (cached && cached.base_url === source.baseUrl && ofThisLogin(cached, record) && isFresh(cached, options.freshForSeconds ?? 30, nowMs)) return cached;
|
|
7826
|
+
const issued = await issueToken(source, record, organizationId, nowMs);
|
|
7827
|
+
saveRotated(source, record.refresh_token, issued.record);
|
|
7828
|
+
saveOrganizationToken(source, organizationId, issued.token);
|
|
7829
|
+
return issued.token;
|
|
7830
|
+
}
|
|
7831
|
+
function ofThisLogin(token, record) {
|
|
7832
|
+
return tokenClaims(token.access_token).sid === tokenClaims(record.refresh_token).sid;
|
|
7833
|
+
}
|
|
7834
|
+
function loginExpiry(record) {
|
|
7835
|
+
const claims = tokenClaims(record.refresh_token);
|
|
7836
|
+
return {
|
|
7837
|
+
idle: typeof claims.exp === "number" ? claims.exp : null,
|
|
7838
|
+
cap: typeof claims.loginEndsAt === "number" ? claims.loginEndsAt : null
|
|
7839
|
+
};
|
|
7747
7840
|
}
|
|
7748
7841
|
async function revokeRefreshToken(baseUrl, clientId2, refreshToken) {
|
|
7749
7842
|
const body = new URLSearchParams({ token: refreshToken, token_type_hint: "refresh_token", client_id: clientId2 });
|
|
@@ -7756,43 +7849,302 @@ async function revokeRefreshToken(baseUrl, clientId2, refreshToken) {
|
|
|
7756
7849
|
if (!response.ok) throw new CliError(`Revoke endpoint returned HTTP ${response.status}`);
|
|
7757
7850
|
}
|
|
7758
7851
|
|
|
7759
|
-
// src/
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7852
|
+
// src/usage.ts
|
|
7853
|
+
function usageEntries(usage2) {
|
|
7854
|
+
const entries = [];
|
|
7855
|
+
let current = null;
|
|
7856
|
+
for (const line of usage2.split("\n")) {
|
|
7857
|
+
if (/^ {2}\S/.test(line)) {
|
|
7858
|
+
const label = line.trim().split(/ {2,}/)[0];
|
|
7859
|
+
const verbs = [];
|
|
7860
|
+
for (const word of label.split(" ")) {
|
|
7861
|
+
if (!/^[a-z][a-z-]*$/.test(word)) break;
|
|
7862
|
+
verbs.push(word);
|
|
7863
|
+
}
|
|
7864
|
+
current = { verbs, summary: line.trim().slice(label.length).trim(), lines: [line] };
|
|
7865
|
+
entries.push(current);
|
|
7866
|
+
} else if (current && /^ {3,}\S/.test(line)) {
|
|
7867
|
+
current.lines.push(line);
|
|
7868
|
+
} else {
|
|
7869
|
+
current = null;
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
return entries;
|
|
7873
|
+
}
|
|
7874
|
+
function startsWith(words, prefix) {
|
|
7875
|
+
return prefix.every((word, index) => words[index] === word);
|
|
7876
|
+
}
|
|
7877
|
+
function verbPath(usage2, args) {
|
|
7878
|
+
let best = [];
|
|
7879
|
+
for (const entry of usageEntries(usage2)) {
|
|
7880
|
+
let shared = 0;
|
|
7881
|
+
while (shared < entry.verbs.length && args[shared] === entry.verbs[shared]) shared++;
|
|
7882
|
+
if (shared > best.length) best = entry.verbs.slice(0, shared);
|
|
7883
|
+
}
|
|
7884
|
+
return best;
|
|
7885
|
+
}
|
|
7886
|
+
function groupOptions(usage2) {
|
|
7887
|
+
return usage2.split("\n", 1)[0].match(/\[--[^\]]+\]/g) ?? [];
|
|
7888
|
+
}
|
|
7889
|
+
function verbUsage(command, usage2, path15) {
|
|
7890
|
+
const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
|
|
7891
|
+
if (path15.length === 0 || lines.length === 0) return usage2;
|
|
7892
|
+
const shared = groupOptions(usage2);
|
|
7893
|
+
return [`diverge ${command} ${path15.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
|
|
7894
|
+
}
|
|
7895
|
+
var COMMON_WORDS = /* @__PURE__ */ new Set(["the", "and", "you", "for", "its", "with", "not", "one", "are", "all", "any", "from", "that", "this", "own", "per", "into", "out", "else", "each", "what", "when", "your"]);
|
|
7896
|
+
function distance(a, b) {
|
|
7897
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
|
|
7898
|
+
for (let i = 1; i <= a.length; i++) {
|
|
7899
|
+
for (let j = 1; j <= b.length; j++) {
|
|
7900
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
7901
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
7902
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
return d[a.length][b.length];
|
|
7906
|
+
}
|
|
7907
|
+
function namesIt(word, guess) {
|
|
7908
|
+
if (word.length < 3 || COMMON_WORDS.has(word)) return false;
|
|
7909
|
+
return word === guess || guess.length >= 3 && word.startsWith(guess) || guess.startsWith(word) || guess.endsWith(word);
|
|
7910
|
+
}
|
|
7911
|
+
function prose(line) {
|
|
7912
|
+
return line.toLowerCase().replace(/\[[^\]]*\]|--?[a-z][a-z-]*/g, " ");
|
|
7913
|
+
}
|
|
7914
|
+
function closest(guess, candidates, rest = [], limit = 3) {
|
|
7915
|
+
const target = guess.toLowerCase().replace(/^-+/, "");
|
|
7916
|
+
const threshold = Math.max(1, Math.floor(target.length / 3));
|
|
7917
|
+
const matches = [];
|
|
7918
|
+
for (const candidate of candidates) {
|
|
7919
|
+
const first = candidate.summary ?? "";
|
|
7920
|
+
const name = candidate.name.toLowerCase();
|
|
7921
|
+
if (name && rest.includes(candidate.name)) {
|
|
7922
|
+
matches.push({ candidate, score: 0, hint: first });
|
|
7923
|
+
continue;
|
|
7924
|
+
}
|
|
7925
|
+
const edits = name ? distance(target, name) : Infinity;
|
|
7926
|
+
if (edits <= threshold || name && target.length >= 2 && name.startsWith(target)) {
|
|
7927
|
+
matches.push({ candidate, score: Math.min(edits, threshold), hint: first });
|
|
7928
|
+
continue;
|
|
7929
|
+
}
|
|
7930
|
+
const line = candidate.lines.find((text) => (prose(text).match(/[a-z]+/g) ?? []).some((word) => namesIt(word, target)));
|
|
7931
|
+
if (line) matches.push({ candidate, score: threshold + 1, hint: line });
|
|
7932
|
+
}
|
|
7933
|
+
return matches.sort((a, b) => a.score - b.score).slice(0, limit);
|
|
7934
|
+
}
|
|
7935
|
+
function didYouMean(matches) {
|
|
7936
|
+
if (matches.length === 0) return "";
|
|
7937
|
+
const shown = (match) => {
|
|
7938
|
+
const hint = match.hint.trim();
|
|
7939
|
+
return hint && hint !== match.candidate.invocation ? `${match.candidate.invocation} (${hint})` : match.candidate.invocation;
|
|
7940
|
+
};
|
|
7941
|
+
if (matches.length === 1) return ` Did you mean: ${shown(matches[0])}?`;
|
|
7942
|
+
return ` Did you mean one of:
|
|
7943
|
+
${matches.map((match) => ` ${shown(match)}`).join("\n")}`;
|
|
7944
|
+
}
|
|
7945
|
+
function verbCandidates(command, usage2, path15) {
|
|
7946
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7947
|
+
for (const entry of usageEntries(usage2)) {
|
|
7948
|
+
if (!startsWith(entry.verbs, path15)) continue;
|
|
7949
|
+
const name = entry.verbs[path15.length] ?? "";
|
|
7950
|
+
const invocation = ["diverge", command, ...path15, ...name ? [name] : []].join(" ");
|
|
7951
|
+
const lines = [entry.summary, ...entry.lines.slice(1).map((line) => line.trim())].filter(Boolean);
|
|
7952
|
+
const known = byName.get(name);
|
|
7953
|
+
if (known) known.lines.push(...lines);
|
|
7954
|
+
else byName.set(name, { invocation, name, summary: entry.summary, lines });
|
|
7955
|
+
}
|
|
7956
|
+
return [...byName.values()];
|
|
7957
|
+
}
|
|
7958
|
+
function unknownVerb(command, usage2, path15, verb, rest = []) {
|
|
7959
|
+
const candidates = verbCandidates(command, usage2, path15);
|
|
7960
|
+
const where = [command, ...path15].join(" ");
|
|
7961
|
+
if (!verb) return new CliError(`${where} needs one of: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(", ")}
|
|
7962
|
+
|
|
7963
|
+
${usage2}`);
|
|
7964
|
+
return new CliError(`Unknown ${where} verb: ${verb}.${didYouMean(closest(verb, candidates, rest))}
|
|
7965
|
+
|
|
7966
|
+
${usage2}`);
|
|
7967
|
+
}
|
|
7968
|
+
function optionCandidates(usage2, path15) {
|
|
7969
|
+
const entries = usageEntries(usage2);
|
|
7970
|
+
const [header = "", ...body] = usage2.split("\n");
|
|
7971
|
+
const described = entries.length === 0 || path15.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
|
|
7972
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7973
|
+
const add = (line, describes) => {
|
|
7974
|
+
for (const [flag] of line.matchAll(/--[a-z][a-z-]*/g)) {
|
|
7975
|
+
const candidate = byName.get(flag) ?? { invocation: flag, name: flag.slice(2), lines: [] };
|
|
7976
|
+
byName.set(flag, candidate);
|
|
7977
|
+
if (describes && !candidate.lines.includes(line.trim())) candidate.lines.push(line.trim());
|
|
7978
|
+
}
|
|
7979
|
+
};
|
|
7980
|
+
add(header, false);
|
|
7981
|
+
for (const line of described) add(line, true);
|
|
7982
|
+
return [...byName.values()];
|
|
7983
|
+
}
|
|
7984
|
+
function unknownOption(error, usage2, args) {
|
|
7985
|
+
if (!(error instanceof Error) || error.name !== "TypeError") return null;
|
|
7986
|
+
const option = /^Unknown option '([^']+)'/.exec(error.message)?.[1];
|
|
7987
|
+
if (!option) return null;
|
|
7988
|
+
const path15 = verbPath(usage2, args);
|
|
7989
|
+
return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path15)))}`;
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
// src/commands/login.ts
|
|
7993
|
+
import { parseArgs } from "node:util";
|
|
7994
|
+
|
|
7995
|
+
// src/auth/organizations.ts
|
|
7996
|
+
function chooseOrganization(input) {
|
|
7997
|
+
const organizations = input.organizations;
|
|
7998
|
+
const flag = input.flag?.trim();
|
|
7999
|
+
if (flag) {
|
|
8000
|
+
const match = organizations.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
|
|
8001
|
+
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
|
|
8002
|
+
throw new CliError(`Not one of your organizations: ${flag}. Yours: ${organizations.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
|
|
8003
|
+
}
|
|
8004
|
+
const remembered = input.remembered?.trim();
|
|
8005
|
+
if (remembered) {
|
|
8006
|
+
const match = organizations.find((organization) => organization.id === remembered);
|
|
8007
|
+
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
|
|
8008
|
+
}
|
|
8009
|
+
if (organizations.length === 1) return { kind: "chosen", organizationId: organizations[0].id, name: organizations[0].name, reason: "only" };
|
|
8010
|
+
if (organizations.length === 0) return { kind: "none" };
|
|
8011
|
+
return { kind: "ask", options: organizations };
|
|
8012
|
+
}
|
|
8013
|
+
function organizationMenu(options) {
|
|
8014
|
+
return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
|
|
7767
8015
|
}
|
|
7768
|
-
function
|
|
7769
|
-
|
|
8016
|
+
function pickFromMenu(answer, options) {
|
|
8017
|
+
const trimmed = answer.trim();
|
|
8018
|
+
const index = Number(trimmed);
|
|
8019
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
|
|
8020
|
+
const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
|
|
8021
|
+
if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
|
|
8022
|
+
return match;
|
|
7770
8023
|
}
|
|
7771
|
-
|
|
7772
|
-
|
|
8024
|
+
|
|
8025
|
+
// src/auth/userinfo.ts
|
|
8026
|
+
async function fetchUserInfo(baseUrl, accessToken) {
|
|
8027
|
+
return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
|
|
7773
8028
|
}
|
|
7774
|
-
function
|
|
8029
|
+
async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
|
|
7775
8030
|
try {
|
|
7776
|
-
const
|
|
7777
|
-
return
|
|
7778
|
-
} catch {
|
|
7779
|
-
return {}
|
|
8031
|
+
const info = await fetchUserInfo(baseUrl, accessToken);
|
|
8032
|
+
return options.withSub ? `${info.email} (${info.sub})` : info.email;
|
|
8033
|
+
} catch (error) {
|
|
8034
|
+
return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
|
|
7780
8035
|
}
|
|
7781
8036
|
}
|
|
7782
|
-
|
|
7783
|
-
|
|
8037
|
+
|
|
8038
|
+
// src/prompt.ts
|
|
8039
|
+
import readline from "node:readline";
|
|
8040
|
+
function ask(question) {
|
|
8041
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8042
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
8043
|
+
rl.close();
|
|
8044
|
+
resolve(answer);
|
|
8045
|
+
}));
|
|
7784
8046
|
}
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
8047
|
+
|
|
8048
|
+
// src/commands/login.ts
|
|
8049
|
+
var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
|
|
8050
|
+
|
|
8051
|
+
Sign in through the browser, then choose where to act. The browser only
|
|
8052
|
+
authenticates the person; the organization is chosen here afterwards \u2014 the
|
|
8053
|
+
one named by --organization-id (id or name), else the last login's, else the
|
|
8054
|
+
only membership, else a menu. The login itself is yours, not an
|
|
8055
|
+
organization's; the default is a local preference.
|
|
8056
|
+
That is the default: commands act there unless they name another with
|
|
8057
|
+
--organization-id or DIVERGENT_ORG_ID. Change it with: diverge org use <id|name>.
|
|
8058
|
+
--as <email> names the account: the platform signs that account in instead of
|
|
8059
|
+
reusing whatever session the browser holds, and issues nothing to any other.
|
|
8060
|
+
Without --as, the account of the last login is used; switching is explicit.
|
|
8061
|
+
Without --scopes, the login asks for the scopes the platform registers for
|
|
8062
|
+
this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
|
|
8063
|
+
scope the platform adds later reaches this login on its next refresh.
|
|
8064
|
+
--scopes asks for less, deliberately, and the login stays that narrow.
|
|
8065
|
+
--api-url signs in to another deployment (a test stack, say). Each deployment
|
|
8066
|
+
keeps its own login, so this never replaces another one; commands reach it
|
|
8067
|
+
when DIVERGENT_API_URL names it.`;
|
|
8068
|
+
function resolveScopes(flag) {
|
|
8069
|
+
const explicit = flag?.trim();
|
|
8070
|
+
return explicit ? explicit.split(/\s+/).join(" ") : void 0;
|
|
7788
8071
|
}
|
|
7789
|
-
function
|
|
7790
|
-
|
|
8072
|
+
function resolveLoginHint(flag, previous) {
|
|
8073
|
+
const explicit = flag?.trim().toLowerCase();
|
|
8074
|
+
if (explicit) return explicit;
|
|
8075
|
+
const remembered = previous.account?.trim().toLowerCase();
|
|
8076
|
+
return remembered || void 0;
|
|
8077
|
+
}
|
|
8078
|
+
async function runLogin(args, io) {
|
|
8079
|
+
const { values } = parseArgs({
|
|
8080
|
+
args,
|
|
8081
|
+
options: {
|
|
8082
|
+
"organization-id": { type: "string" },
|
|
8083
|
+
scopes: { type: "string" },
|
|
8084
|
+
as: { type: "string" },
|
|
8085
|
+
"no-browser": { type: "boolean", default: false },
|
|
8086
|
+
"api-url": { type: "string" }
|
|
8087
|
+
},
|
|
8088
|
+
strict: true,
|
|
8089
|
+
allowPositionals: false
|
|
8090
|
+
});
|
|
8091
|
+
const apiUrl = resolveApiUrl(values["api-url"], io.env);
|
|
8092
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8093
|
+
const previous = loadLogin(source);
|
|
8094
|
+
const loginHint = resolveLoginHint(values.as, { account: previous?.account });
|
|
8095
|
+
if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
|
|
8096
|
+
const scopes = resolveScopes(values.scopes);
|
|
8097
|
+
io.stdout(scopes ? `Asking for scopes: ${scopes}.` : "Asking for the scopes the platform registers for this CLI.");
|
|
8098
|
+
const signIn = await login({
|
|
8099
|
+
baseUrl: apiUrl,
|
|
8100
|
+
clientId: source.clientId,
|
|
8101
|
+
scopes,
|
|
8102
|
+
openBrowser: !values["no-browser"],
|
|
8103
|
+
loginHint,
|
|
8104
|
+
print: io.stdout
|
|
8105
|
+
});
|
|
8106
|
+
let info;
|
|
8107
|
+
try {
|
|
8108
|
+
info = await fetchUserInfo(apiUrl, signIn.token.access_token);
|
|
8109
|
+
} catch (error) {
|
|
8110
|
+
throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
|
|
8111
|
+
}
|
|
8112
|
+
io.stdout(`Signed in as: ${info.email}`);
|
|
8113
|
+
const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
|
|
8114
|
+
const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
|
|
8115
|
+
saveLogin(source, { ...issued.record, organization: organizationId });
|
|
8116
|
+
saveOrganizationToken(source, organizationId, issued.token);
|
|
8117
|
+
io.stdout(`Logged in to ${apiUrl}.`);
|
|
8118
|
+
if (apiUrl !== resolveApiUrl(void 0, io.env)) io.stdout(`Commands reach this deployment with DIVERGENT_API_URL=${apiUrl}.`);
|
|
8119
|
+
const granted = new Set(tokenClaims(issued.token.access_token).scopes ?? []);
|
|
8120
|
+
io.stdout(`Scopes: ${[...granted].join(" ") || "none"}`);
|
|
8121
|
+
const missing = (scopes ?? "").split(" ").filter((scope) => scope && !granted.has(scope));
|
|
8122
|
+
if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
|
|
8123
|
+
return 0;
|
|
8124
|
+
}
|
|
8125
|
+
async function chooseDefaultOrganization(io, flag, remembered, info) {
|
|
8126
|
+
let choice = chooseOrganization({ flag, remembered, organizations: info.organizations ?? [] });
|
|
8127
|
+
if (choice.kind === "none") {
|
|
8128
|
+
throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
|
|
8129
|
+
}
|
|
8130
|
+
if (choice.kind === "ask") {
|
|
8131
|
+
if (!io.interactive) {
|
|
8132
|
+
throw new CliError(`You may act in several organizations; pass --organization-id <id|name>:
|
|
8133
|
+
${organizationMenu(choice.options).join("\n")}`);
|
|
8134
|
+
}
|
|
8135
|
+
io.stdout("Choose the organization to act in:");
|
|
8136
|
+
for (const line of organizationMenu(choice.options)) io.stdout(line);
|
|
8137
|
+
const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
|
|
8138
|
+
choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
|
|
8139
|
+
}
|
|
8140
|
+
const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
|
|
8141
|
+
const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
|
|
8142
|
+
io.stdout(`Organization: ${label}${why}`);
|
|
8143
|
+
return choice.organizationId;
|
|
7791
8144
|
}
|
|
7792
8145
|
|
|
7793
8146
|
// src/gateway/urls.ts
|
|
7794
8147
|
var SURFACES = ["anthropic", "openai"];
|
|
7795
|
-
var GATEWAY_SCOPES = "mcp:read mcp:execute inference:execute";
|
|
7796
8148
|
var INFERENCE_SCOPE = "inference:execute";
|
|
7797
8149
|
function anthropicBaseUrl(apiUrl) {
|
|
7798
8150
|
return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/anthropic`;
|
|
@@ -7803,12 +8155,6 @@ function openaiBaseUrl(apiUrl) {
|
|
|
7803
8155
|
function surfaceBaseUrl(apiUrl, surface) {
|
|
7804
8156
|
return surface === "openai" ? openaiBaseUrl(apiUrl) : anthropicBaseUrl(apiUrl);
|
|
7805
8157
|
}
|
|
7806
|
-
function loginServesGateway(claims, organizationId) {
|
|
7807
|
-
if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) return false;
|
|
7808
|
-
const org = claims.organizationId;
|
|
7809
|
-
if (!org) return false;
|
|
7810
|
-
return !organizationId || org === organizationId;
|
|
7811
|
-
}
|
|
7812
8158
|
|
|
7813
8159
|
// src/gateway/models.ts
|
|
7814
8160
|
async function listModels(apiUrl, accessToken, surface) {
|
|
@@ -7825,35 +8171,27 @@ async function listModels(apiUrl, accessToken, surface) {
|
|
|
7825
8171
|
throw error;
|
|
7826
8172
|
}
|
|
7827
8173
|
}
|
|
7828
|
-
async function ensureInferenceLogin(io, apiUrl, organizationId
|
|
7829
|
-
const source =
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
} catch (error) {
|
|
7834
|
-
if (!(error instanceof CliError)) throw error;
|
|
8174
|
+
async function ensureInferenceLogin(io, apiUrl, organizationId) {
|
|
8175
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8176
|
+
if (!loadLogin(source)) {
|
|
8177
|
+
if (!io.interactive) throw new CliError(`Not signed in to ${apiUrl}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
|
|
8178
|
+
await runLogin(organizationId ? ["--organization-id", organizationId] : [], io);
|
|
7835
8179
|
}
|
|
7836
|
-
|
|
7837
|
-
|
|
8180
|
+
const token = await getUserToken(source, { organizationId });
|
|
8181
|
+
const claims = tokenClaims(token.access_token);
|
|
8182
|
+
if (!claims.organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
|
|
8183
|
+
if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) {
|
|
8184
|
+
throw new CliError(`The token for organization ${claims.organizationId} carries no ${INFERENCE_SCOPE}: your roles there lack inference_models:execute, or the login asked for less (re-run: diverge login).`);
|
|
7838
8185
|
}
|
|
7839
|
-
return
|
|
7840
|
-
baseUrl: apiUrl,
|
|
7841
|
-
clientId: source.clientId,
|
|
7842
|
-
scopes: GATEWAY_SCOPES,
|
|
7843
|
-
organizationId,
|
|
7844
|
-
selectOrganization: !organizationId,
|
|
7845
|
-
openBrowser: openBrowser2,
|
|
7846
|
-
tokenFile: source.tokenFile,
|
|
7847
|
-
print: io.stdout
|
|
7848
|
-
});
|
|
8186
|
+
return token;
|
|
7849
8187
|
}
|
|
7850
8188
|
|
|
7851
8189
|
// src/agents/claude.ts
|
|
7852
8190
|
import os2 from "node:os";
|
|
7853
|
-
import
|
|
8191
|
+
import path4 from "node:path";
|
|
7854
8192
|
var CLAUDE_MODEL_DISCOVERY_FLAG = "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
|
|
7855
8193
|
function claudeSettingsPath(scope, cwd = process.cwd()) {
|
|
7856
|
-
return scope === "user" ?
|
|
8194
|
+
return scope === "user" ? path4.join(os2.homedir(), ".claude", "settings.json") : path4.join(cwd, ".claude", "settings.local.json");
|
|
7857
8195
|
}
|
|
7858
8196
|
function mergeClaudeSettings(existing, options) {
|
|
7859
8197
|
const settings = structuredClone(existing ?? {});
|
|
@@ -7874,11 +8212,11 @@ function mergeClaudeSettings(existing, options) {
|
|
|
7874
8212
|
|
|
7875
8213
|
// src/agents/codex.ts
|
|
7876
8214
|
import os3 from "node:os";
|
|
7877
|
-
import
|
|
8215
|
+
import path5 from "node:path";
|
|
7878
8216
|
var CODEX_BLOCK_START = "# >>> divergent >>>";
|
|
7879
8217
|
var CODEX_BLOCK_END = "# <<< divergent <<<";
|
|
7880
8218
|
function codexConfigPath() {
|
|
7881
|
-
return
|
|
8219
|
+
return path5.join(os3.homedir(), ".codex", "config.toml");
|
|
7882
8220
|
}
|
|
7883
8221
|
var tomlString = (value) => JSON.stringify(value);
|
|
7884
8222
|
function codexProviderToml(options) {
|
|
@@ -7894,7 +8232,7 @@ function codexProviderToml(options) {
|
|
|
7894
8232
|
"",
|
|
7895
8233
|
"[model_providers.divergent.auth]",
|
|
7896
8234
|
`command = ${tomlString(options.self.command)}`,
|
|
7897
|
-
`args = [${[...options.self.args, "token"].map(tomlString).join(", ")}]`,
|
|
8235
|
+
`args = [${[...options.self.args, "token", ...options.organizationId ? ["--organization-id", options.organizationId] : []].map(tomlString).join(", ")}]`,
|
|
7898
8236
|
"refresh_interval_ms = 600000",
|
|
7899
8237
|
CODEX_BLOCK_END,
|
|
7900
8238
|
""
|
|
@@ -7919,10 +8257,10 @@ var import_yaml = __toESM(require_dist(), 1);
|
|
|
7919
8257
|
import fs4 from "node:fs";
|
|
7920
8258
|
import { createRequire } from "node:module";
|
|
7921
8259
|
import os4 from "node:os";
|
|
7922
|
-
import
|
|
8260
|
+
import path6 from "node:path";
|
|
7923
8261
|
var DSH_SHIPPED_DEFAULT_MODEL = "deepseek-flash";
|
|
7924
8262
|
function dshHome(env = process.env) {
|
|
7925
|
-
return env.DSH_HOME?.trim() ||
|
|
8263
|
+
return env.DSH_HOME?.trim() || path6.join(os4.homedir(), ".dsh");
|
|
7926
8264
|
}
|
|
7927
8265
|
function dshDefaultModel(models, requested) {
|
|
7928
8266
|
if (requested) return requested;
|
|
@@ -8011,30 +8349,30 @@ var DIVERGENT_PRESET_ID = "divergent";
|
|
|
8011
8349
|
var DSH_USER_PRESETS_DIR = ".agent-presets";
|
|
8012
8350
|
function dshPackageDir(binary) {
|
|
8013
8351
|
if (/\.(cmd|bat)$/i.test(binary)) {
|
|
8014
|
-
const dir2 =
|
|
8015
|
-
return fs4.existsSync(
|
|
8352
|
+
const dir2 = path6.join(path6.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
|
|
8353
|
+
return fs4.existsSync(path6.join(dir2, "package.json")) ? dir2 : null;
|
|
8016
8354
|
}
|
|
8017
8355
|
let dir;
|
|
8018
8356
|
try {
|
|
8019
|
-
dir =
|
|
8357
|
+
dir = path6.dirname(fs4.realpathSync(binary));
|
|
8020
8358
|
} catch {
|
|
8021
8359
|
return null;
|
|
8022
8360
|
}
|
|
8023
8361
|
for (; ; ) {
|
|
8024
8362
|
try {
|
|
8025
|
-
if (JSON.parse(fs4.readFileSync(
|
|
8363
|
+
if (JSON.parse(fs4.readFileSync(path6.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
|
|
8026
8364
|
} catch {
|
|
8027
8365
|
}
|
|
8028
|
-
const parent =
|
|
8366
|
+
const parent = path6.dirname(dir);
|
|
8029
8367
|
if (parent === dir) return null;
|
|
8030
8368
|
dir = parent;
|
|
8031
8369
|
}
|
|
8032
8370
|
}
|
|
8033
8371
|
function shippedPresetDir(packageDir, id = "standard") {
|
|
8034
8372
|
try {
|
|
8035
|
-
const require2 = createRequire(
|
|
8036
|
-
const dir =
|
|
8037
|
-
return fs4.existsSync(
|
|
8373
|
+
const require2 = createRequire(path6.join(packageDir, "package.json"));
|
|
8374
|
+
const dir = path6.join(path6.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
|
|
8375
|
+
return fs4.existsSync(path6.join(dir, "agent.cordis.yml")) ? dir : null;
|
|
8038
8376
|
} catch {
|
|
8039
8377
|
return null;
|
|
8040
8378
|
}
|
|
@@ -8071,9 +8409,9 @@ var DIVERGENT_PRESET_METADATA = [
|
|
|
8071
8409
|
""
|
|
8072
8410
|
].join("\n");
|
|
8073
8411
|
function writeDivergentPreset(home, shippedDir) {
|
|
8074
|
-
const dir =
|
|
8075
|
-
writeTextAtomic(
|
|
8076
|
-
writeTextAtomic(
|
|
8412
|
+
const dir = path6.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
|
|
8413
|
+
writeTextAtomic(path6.join(dir, "agent.cordis.yml"), divergentPreset(fs4.readFileSync(path6.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
|
|
8414
|
+
writeTextAtomic(path6.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
|
|
8077
8415
|
return dir;
|
|
8078
8416
|
}
|
|
8079
8417
|
function withDivergentPresetDefault(settingsText) {
|
|
@@ -8087,9 +8425,9 @@ function withDivergentPresetDefault(settingsText) {
|
|
|
8087
8425
|
|
|
8088
8426
|
// src/agents/opencode.ts
|
|
8089
8427
|
import os5 from "node:os";
|
|
8090
|
-
import
|
|
8428
|
+
import path7 from "node:path";
|
|
8091
8429
|
function opencodeConfigPath() {
|
|
8092
|
-
return
|
|
8430
|
+
return path7.join(os5.homedir(), ".config", "opencode", "opencode.json");
|
|
8093
8431
|
}
|
|
8094
8432
|
function mergeOpencodeConfig(existing, options) {
|
|
8095
8433
|
const tokenEnv = options.tokenEnv ?? "DIVERGENT_TOKEN";
|
|
@@ -8106,14 +8444,14 @@ function mergeOpencodeConfig(existing, options) {
|
|
|
8106
8444
|
// src/agents/run-dsh.ts
|
|
8107
8445
|
import { spawn as spawn2 } from "node:child_process";
|
|
8108
8446
|
import fs5 from "node:fs";
|
|
8109
|
-
import
|
|
8447
|
+
import path8 from "node:path";
|
|
8110
8448
|
function findOnPath(name, env = process.env, platform = process.platform) {
|
|
8111
|
-
const dirs = (env.PATH ?? "").split(
|
|
8449
|
+
const dirs = (env.PATH ?? "").split(path8.delimiter).filter(Boolean);
|
|
8112
8450
|
const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
8113
8451
|
const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
|
|
8114
8452
|
for (const dir of dirs) {
|
|
8115
8453
|
for (const candidate of candidates) {
|
|
8116
|
-
const full =
|
|
8454
|
+
const full = path8.join(dir, candidate);
|
|
8117
8455
|
try {
|
|
8118
8456
|
fs5.accessSync(full, fs5.constants.X_OK);
|
|
8119
8457
|
if (fs5.statSync(full).isFile()) return full;
|
|
@@ -8139,16 +8477,16 @@ function dshArgs(overlayPath, args) {
|
|
|
8139
8477
|
}
|
|
8140
8478
|
function dshLaunch(binary, args, platform = process.platform, execPath = process.execPath) {
|
|
8141
8479
|
if (platform !== "win32" || !/\.(cmd|bat)$/i.test(binary)) return { command: binary, args };
|
|
8142
|
-
const packageDir =
|
|
8480
|
+
const packageDir = path8.join(path8.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
|
|
8143
8481
|
let bin;
|
|
8144
8482
|
try {
|
|
8145
|
-
bin = JSON.parse(fs5.readFileSync(
|
|
8483
|
+
bin = JSON.parse(fs5.readFileSync(path8.join(packageDir, "package.json"), "utf8")).bin;
|
|
8146
8484
|
} catch {
|
|
8147
8485
|
throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
|
|
8148
8486
|
}
|
|
8149
8487
|
const script = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null ? bin.dsh : void 0;
|
|
8150
8488
|
if (typeof script !== "string") throw new CliError(`${packageDir}/package.json declares no dsh entry; reinstall: npm install -g @deepseek-ai/dsh`);
|
|
8151
|
-
return { command: execPath, args: [
|
|
8489
|
+
return { command: execPath, args: [path8.join(packageDir, script), ...args] };
|
|
8152
8490
|
}
|
|
8153
8491
|
async function keepTokenFresh(loop) {
|
|
8154
8492
|
let written = null;
|
|
@@ -8215,7 +8553,7 @@ function runDsh(options) {
|
|
|
8215
8553
|
}
|
|
8216
8554
|
|
|
8217
8555
|
// src/commands/skills.ts
|
|
8218
|
-
import { parseArgs } from "node:util";
|
|
8556
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
8219
8557
|
|
|
8220
8558
|
// src/skills/bundled.ts
|
|
8221
8559
|
var DIVERGE_SKILL = {
|
|
@@ -8235,9 +8573,9 @@ their permissions and audit trail. Nothing runs locally; the CLI is the
|
|
|
8235
8573
|
transport. Keep the catalog out of context: pull the one schema you need.
|
|
8236
8574
|
|
|
8237
8575
|
\`\`\`bash
|
|
8238
|
-
diverge tools list --filter <word> # find candidates by name or
|
|
8576
|
+
diverge tools list --filter <word> # find candidates by name or description
|
|
8239
8577
|
diverge tools describe <tool> # the one schema, on demand
|
|
8240
|
-
diverge tools call <tool> --args-file args.json # arguments from a file
|
|
8578
|
+
diverge tools call <tool> --args-file args.json --json # arguments from a file, one JSON outcome out
|
|
8241
8579
|
diverge tools call search_online --args-json '{"query": "..."}' # web search is a platform tool too
|
|
8242
8580
|
\`\`\`
|
|
8243
8581
|
|
|
@@ -8253,20 +8591,28 @@ Write the object to a file and pass \`--args-file\`, or pipe it with
|
|
|
8253
8591
|
\`--args-json -\`; both are exact on every shell. A worked call:
|
|
8254
8592
|
|
|
8255
8593
|
\`\`\`bash
|
|
8256
|
-
diverge tools describe divix-
|
|
8594
|
+
diverge tools describe divix-data-sync_implementation_search_source
|
|
8257
8595
|
cat > /tmp/args.json <<'JSON'
|
|
8258
8596
|
{"query": "cr9b5_serial", "limit": 20}
|
|
8259
8597
|
JSON
|
|
8260
|
-
diverge tools call divix-
|
|
8598
|
+
diverge tools call divix-data-sync_implementation_search_source --args-file /tmp/args.json
|
|
8261
8599
|
\`\`\`
|
|
8262
8600
|
|
|
8601
|
+
## Read the outcome from stdout with --json
|
|
8602
|
+
|
|
8603
|
+
\`call --json\` prints exactly one JSON object on stdout whatever happens,
|
|
8604
|
+
errors included: \`{"tool", "status", "result"?, "error"?, "invocationId"?,
|
|
8605
|
+
"decideAt"?}\`, where status is executed, pending, rejected, expired or failed.
|
|
8606
|
+
Parse stdout alone; stderr only carries the scope and identity lines. Without
|
|
8607
|
+
\`--json\`, errors are text on stderr.
|
|
8608
|
+
|
|
8263
8609
|
## Several calls at once
|
|
8264
8610
|
|
|
8265
8611
|
When a task needs a sequence of calls, write them to one file and run it
|
|
8266
|
-
once instead of one round trip each: \`diverge tools run calls.json
|
|
8612
|
+
once instead of one round trip each: \`diverge tools run calls.json\` (or \`run -\` for stdin), a JSON
|
|
8267
8613
|
array of \`{"tool": "...", "args": {...}}\` (or one per line). Calls run in
|
|
8268
|
-
order; each prints one JSON line of outcome
|
|
8269
|
-
them.
|
|
8614
|
+
order; each prints one JSON line of outcome, the same shape as \`call --json\`;
|
|
8615
|
+
the exit code is the worst of them.
|
|
8270
8616
|
|
|
8271
8617
|
## Exit codes are the contract
|
|
8272
8618
|
|
|
@@ -8284,8 +8630,11 @@ cadence). Never decide your own invocation from any surface you can drive.
|
|
|
8284
8630
|
|
|
8285
8631
|
## Session
|
|
8286
8632
|
|
|
8287
|
-
- \`diverge status\` says who is signed in and at which organization;
|
|
8288
|
-
|
|
8633
|
+
- \`diverge status\` says who is signed in and at which organization; a call
|
|
8634
|
+
that names none runs there. To act in another organization, pass
|
|
8635
|
+
\`--organization-id <id>\` (or set \`DIVERGENT_ORG_ID\`) on the call; that
|
|
8636
|
+
never changes the default. \`diverge org use <id|name>\` changes it for
|
|
8637
|
+
every session on the machine, so leave it to the person.
|
|
8289
8638
|
- A missing login or scope prints the command to run; \`diverge login\` opens
|
|
8290
8639
|
the browser.
|
|
8291
8640
|
- \`--json\` on \`list\`, \`describe\` and \`invocation\` gives raw records.
|
|
@@ -8295,22 +8644,22 @@ cadence). Never decide your own invocation from any surface you can drive.
|
|
|
8295
8644
|
// src/skills/install.ts
|
|
8296
8645
|
import fs6 from "node:fs";
|
|
8297
8646
|
import os6 from "node:os";
|
|
8298
|
-
import
|
|
8647
|
+
import path9 from "node:path";
|
|
8299
8648
|
var HARNESSES = ["claude", "codex", "dsh"];
|
|
8300
8649
|
var SKILLS_MANIFEST = ".diverge-skills.json";
|
|
8301
8650
|
function skillsRoot(harness, env = process.env) {
|
|
8302
8651
|
switch (harness) {
|
|
8303
8652
|
case "claude":
|
|
8304
|
-
return
|
|
8653
|
+
return path9.join(env.CLAUDE_CONFIG_DIR?.trim() || path9.join(os6.homedir(), ".claude"), "skills");
|
|
8305
8654
|
case "codex":
|
|
8306
|
-
return
|
|
8655
|
+
return path9.join(env.CODEX_HOME?.trim() || path9.join(os6.homedir(), ".codex"), "skills");
|
|
8307
8656
|
case "dsh":
|
|
8308
|
-
return
|
|
8657
|
+
return path9.join(dshHome(env), "skills");
|
|
8309
8658
|
}
|
|
8310
8659
|
}
|
|
8311
8660
|
function readSkillsManifest(root) {
|
|
8312
8661
|
try {
|
|
8313
|
-
const parsed = JSON.parse(fs6.readFileSync(
|
|
8662
|
+
const parsed = JSON.parse(fs6.readFileSync(path9.join(root, SKILLS_MANIFEST), "utf8"));
|
|
8314
8663
|
if (parsed && typeof parsed === "object" && typeof parsed.skills === "object") return parsed;
|
|
8315
8664
|
} catch {
|
|
8316
8665
|
}
|
|
@@ -8327,7 +8676,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8327
8676
|
const updated = [];
|
|
8328
8677
|
for (const skill of skills) {
|
|
8329
8678
|
if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(skill.name)) throw new Error(`skill name ${JSON.stringify(skill.name)} is not a folder name`);
|
|
8330
|
-
writeTextAtomic(
|
|
8679
|
+
writeTextAtomic(path9.join(root, skill.name, "SKILL.md"), skill.content, 420);
|
|
8331
8680
|
written.push(skill.name);
|
|
8332
8681
|
if (!(skill.name in held)) added.push(skill.name);
|
|
8333
8682
|
else if (held[skill.name] !== skill.updatedAt) updated.push(skill.name);
|
|
@@ -8335,7 +8684,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8335
8684
|
const removed = [];
|
|
8336
8685
|
for (const name of Object.keys(previous?.skills ?? {})) {
|
|
8337
8686
|
if (skills.some((skill) => skill.name === name)) continue;
|
|
8338
|
-
fs6.rmSync(
|
|
8687
|
+
fs6.rmSync(path9.join(root, name), { recursive: true, force: true });
|
|
8339
8688
|
removed.push(name);
|
|
8340
8689
|
}
|
|
8341
8690
|
const manifest = {
|
|
@@ -8343,7 +8692,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8343
8692
|
source,
|
|
8344
8693
|
skills: Object.fromEntries(skills.map((skill) => [skill.name, skill.updatedAt]))
|
|
8345
8694
|
};
|
|
8346
|
-
writeJsonAtomic(
|
|
8695
|
+
writeJsonAtomic(path9.join(root, SKILLS_MANIFEST), manifest, 420);
|
|
8347
8696
|
return { root, written, added, updated, removed };
|
|
8348
8697
|
}
|
|
8349
8698
|
function skillsStatus(root, skills) {
|
|
@@ -8353,7 +8702,7 @@ function skillsStatus(root, skills) {
|
|
|
8353
8702
|
const behind = [];
|
|
8354
8703
|
const missing = [];
|
|
8355
8704
|
for (const skill of skills) {
|
|
8356
|
-
if (!(skill.name in held) || !fs6.existsSync(
|
|
8705
|
+
if (!(skill.name in held) || !fs6.existsSync(path9.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
|
|
8357
8706
|
else if (held[skill.name] !== skill.updatedAt) behind.push(skill.name);
|
|
8358
8707
|
else current.push(skill.name);
|
|
8359
8708
|
}
|
|
@@ -8383,10 +8732,9 @@ var skillsUsage = `diverge skills <list|install|status> [--harness claude,codex,
|
|
|
8383
8732
|
A skill is a procedure over the tools it names; you get the ones whose every
|
|
8384
8733
|
tool you may run. Re-run install after the organization changes its skills.`;
|
|
8385
8734
|
async function installableSkillSet(apiUrl, env) {
|
|
8386
|
-
const
|
|
8387
|
-
const token = await getUserToken(source);
|
|
8735
|
+
const token = await getUserToken(tokenSource(env, apiUrl));
|
|
8388
8736
|
const organizationId = tokenClaims(token.access_token).organizationId;
|
|
8389
|
-
if (!organizationId) throw new CliError("The login
|
|
8737
|
+
if (!organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
|
|
8390
8738
|
const served = await fetchInstallableSkills(apiUrl, token.access_token);
|
|
8391
8739
|
return { skills: [DIVERGE_SKILL, ...served.skills], organizationId };
|
|
8392
8740
|
}
|
|
@@ -8408,7 +8756,7 @@ function installForHarness(io, harness, skills, apiUrl, into) {
|
|
|
8408
8756
|
}
|
|
8409
8757
|
async function runSkills(args, io) {
|
|
8410
8758
|
const [verb, ...rest] = args;
|
|
8411
|
-
const { values } =
|
|
8759
|
+
const { values } = parseArgs2({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
|
|
8412
8760
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8413
8761
|
switch (verb) {
|
|
8414
8762
|
case "list": {
|
|
@@ -8439,17 +8787,15 @@ async function runSkills(args, io) {
|
|
|
8439
8787
|
return ExitCode.Ok;
|
|
8440
8788
|
}
|
|
8441
8789
|
default:
|
|
8442
|
-
throw
|
|
8443
|
-
|
|
8444
|
-
${skillsUsage}`);
|
|
8790
|
+
throw unknownVerb("skills", skillsUsage, [], verb, rest);
|
|
8445
8791
|
}
|
|
8446
8792
|
}
|
|
8447
8793
|
|
|
8448
8794
|
// src/agents/rotate.ts
|
|
8449
8795
|
import os7 from "node:os";
|
|
8450
|
-
import
|
|
8796
|
+
import path10 from "node:path";
|
|
8451
8797
|
function defaultTokenFilePath() {
|
|
8452
|
-
return
|
|
8798
|
+
return path10.join(os7.homedir(), ".divergent", "token");
|
|
8453
8799
|
}
|
|
8454
8800
|
var MIN_ROTATE_INTERVAL_SECONDS = 60;
|
|
8455
8801
|
async function rotateTokenFile(loop) {
|
|
@@ -8465,14 +8811,14 @@ async function rotateTokenFile(loop) {
|
|
|
8465
8811
|
|
|
8466
8812
|
// src/agents/self-command.ts
|
|
8467
8813
|
import fs7 from "node:fs";
|
|
8468
|
-
import
|
|
8814
|
+
import path11 from "node:path";
|
|
8469
8815
|
function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
|
|
8470
8816
|
const script = argv[1];
|
|
8471
8817
|
if (!script) return { command: execPath, args: [] };
|
|
8472
8818
|
if (platform !== "win32") {
|
|
8473
8819
|
try {
|
|
8474
8820
|
fs7.accessSync(script, fs7.constants.X_OK);
|
|
8475
|
-
return { command:
|
|
8821
|
+
return { command: path11.resolve(script), args: [] };
|
|
8476
8822
|
} catch {
|
|
8477
8823
|
}
|
|
8478
8824
|
}
|
|
@@ -8521,7 +8867,7 @@ function syncDivergentPreset(io, binary, home) {
|
|
|
8521
8867
|
return;
|
|
8522
8868
|
}
|
|
8523
8869
|
const dir = writeDivergentPreset(home, shipped);
|
|
8524
|
-
const settingsPath =
|
|
8870
|
+
const settingsPath = path12.join(home, "settings.yaml");
|
|
8525
8871
|
const settings = withDivergentPresetDefault(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null);
|
|
8526
8872
|
if (settings !== null) writeTextAtomic(settingsPath, settings);
|
|
8527
8873
|
io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
|
|
@@ -8541,18 +8887,22 @@ async function overlayAndDefault(apiUrl, token, surface, io, requested) {
|
|
|
8541
8887
|
const defaultModel = dshDefaultModel(models, requested);
|
|
8542
8888
|
return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models, defaultModel }), defaultModel, models };
|
|
8543
8889
|
}
|
|
8890
|
+
function organizationArgs(organizationId) {
|
|
8891
|
+
const trimmed = organizationId?.trim();
|
|
8892
|
+
return trimmed ? ["--organization-id", trimmed] : [];
|
|
8893
|
+
}
|
|
8544
8894
|
async function setup(args, io) {
|
|
8545
8895
|
const [agent, ...rest] = args;
|
|
8546
8896
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8547
8897
|
switch (agent) {
|
|
8548
8898
|
case "claude": {
|
|
8549
|
-
const { values } =
|
|
8899
|
+
const { values } = parseArgs3({ args: rest, options: { scope: { type: "string", default: "user" }, "project-id": { type: "string" }, "organization-id": { type: "string" }, "model-discovery": { type: "boolean", default: false } }, strict: true });
|
|
8550
8900
|
if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
|
|
8551
8901
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8552
8902
|
const file = claudeSettingsPath(values.scope);
|
|
8553
8903
|
const { settings, warnings } = mergeClaudeSettings(readJson(file), {
|
|
8554
8904
|
baseUrl: anthropicBaseUrl(apiUrl),
|
|
8555
|
-
tokenHelper: shellCommand(resolveSelfCommand(), "token"),
|
|
8905
|
+
tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
|
|
8556
8906
|
projectId: values["project-id"],
|
|
8557
8907
|
modelDiscovery: values["model-discovery"]
|
|
8558
8908
|
});
|
|
@@ -8563,7 +8913,7 @@ async function setup(args, io) {
|
|
|
8563
8913
|
return ExitCode.Ok;
|
|
8564
8914
|
}
|
|
8565
8915
|
case "opencode": {
|
|
8566
|
-
const { values } =
|
|
8916
|
+
const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
|
|
8567
8917
|
const surface = surfaceOf(values.surface, "anthropic");
|
|
8568
8918
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8569
8919
|
const baseUrl = surfaceBaseUrl(apiUrl, surface);
|
|
@@ -8577,12 +8927,12 @@ async function setup(args, io) {
|
|
|
8577
8927
|
writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
|
|
8578
8928
|
io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
|
|
8579
8929
|
io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
|
|
8580
|
-
io.stdout(
|
|
8930
|
+
io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
|
|
8581
8931
|
return ExitCode.Ok;
|
|
8582
8932
|
}
|
|
8583
8933
|
case "codex": {
|
|
8584
|
-
const { values } =
|
|
8585
|
-
const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"] });
|
|
8934
|
+
const { values } = parseArgs3({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
|
|
8935
|
+
const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"], organizationId: values["organization-id"] });
|
|
8586
8936
|
if (values.print) {
|
|
8587
8937
|
io.stdout(block.trimEnd());
|
|
8588
8938
|
return ExitCode.Ok;
|
|
@@ -8597,7 +8947,7 @@ async function setup(args, io) {
|
|
|
8597
8947
|
return ExitCode.Ok;
|
|
8598
8948
|
}
|
|
8599
8949
|
case "dsh": {
|
|
8600
|
-
const { values } =
|
|
8950
|
+
const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" }, into: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
|
|
8601
8951
|
const surface = surfaceOf(values.surface, "openai");
|
|
8602
8952
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8603
8953
|
if (!values.print) await skillsFor(io, apiUrl, "dsh");
|
|
@@ -8606,31 +8956,27 @@ async function setup(args, io) {
|
|
|
8606
8956
|
io.stdout(overlay.trimEnd());
|
|
8607
8957
|
return ExitCode.Ok;
|
|
8608
8958
|
}
|
|
8609
|
-
const file = values.into ??
|
|
8959
|
+
const file = values.into ?? path12.join(dshHome(io.env), `divergent-${surface}.yml`);
|
|
8610
8960
|
writeTextAtomic(file, overlay);
|
|
8611
8961
|
io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
|
|
8612
8962
|
io.stdout("Platform tools, web search included, are reached from dsh's shell through `diverge tools`; the installed skills say when and how. dsh's DeepSeek search is off.");
|
|
8613
8963
|
syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
|
|
8614
8964
|
io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
|
|
8615
|
-
io.stdout(` DIVERGENT_TOKEN="$(diverge token)" dsh --profile headless --patch ${file} "<task>"`);
|
|
8965
|
+
io.stdout(` DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})" dsh --profile headless --patch ${file} "<task>"`);
|
|
8616
8966
|
io.stdout(`or let the CLI keep the token fresh for the whole session: diverge agents run dsh --surface ${surface} -- --profile web --port 3131 --no-open`);
|
|
8617
8967
|
return ExitCode.Ok;
|
|
8618
8968
|
}
|
|
8619
8969
|
default:
|
|
8620
|
-
throw
|
|
8621
|
-
|
|
8622
|
-
${agentsUsage}`);
|
|
8970
|
+
throw unknownVerb("agents", agentsUsage, ["setup"], agent, rest);
|
|
8623
8971
|
}
|
|
8624
8972
|
}
|
|
8625
8973
|
async function run(args, io) {
|
|
8626
8974
|
const [agent, ...rest] = args;
|
|
8627
|
-
if (agent !== "dsh") throw
|
|
8628
|
-
|
|
8629
|
-
${agentsUsage}`);
|
|
8975
|
+
if (agent !== "dsh") throw unknownVerb("agents", agentsUsage, ["run"], agent, rest);
|
|
8630
8976
|
const terminator = rest.indexOf("--");
|
|
8631
8977
|
const flags = terminator === -1 ? rest : rest.slice(0, terminator);
|
|
8632
8978
|
const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
|
|
8633
|
-
const { values } =
|
|
8979
|
+
const { values } = parseArgs3({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
|
|
8634
8980
|
const surface = surfaceOf(values.surface, "openai");
|
|
8635
8981
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8636
8982
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
@@ -8639,7 +8985,7 @@ ${agentsUsage}`);
|
|
|
8639
8985
|
await skillsFor(io, apiUrl, "dsh");
|
|
8640
8986
|
const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
|
|
8641
8987
|
const home = dshHome(io.env);
|
|
8642
|
-
const settingsPath =
|
|
8988
|
+
const settingsPath = path12.join(home, "settings.yaml");
|
|
8643
8989
|
let pinned;
|
|
8644
8990
|
try {
|
|
8645
8991
|
pinned = dshSettingsDefaultModel(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null, surface);
|
|
@@ -8648,22 +8994,22 @@ ${agentsUsage}`);
|
|
|
8648
8994
|
}
|
|
8649
8995
|
const served = servedIds(overlay);
|
|
8650
8996
|
if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
|
|
8651
|
-
const overlayPath =
|
|
8997
|
+
const overlayPath = path12.join(home, `divergent-${surface}.yml`);
|
|
8652
8998
|
writeTextAtomic(overlayPath, overlay);
|
|
8653
8999
|
syncDivergentPreset(io, binary, home);
|
|
8654
9000
|
io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
|
|
8655
9001
|
if (passthrough.length === 0) io.stderr('Starting the web UI; open the URL dsh prints. For one task without the UI: diverge agents run dsh -- "<task>"');
|
|
8656
|
-
const source =
|
|
9002
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8657
9003
|
return runDsh({
|
|
8658
9004
|
binary,
|
|
8659
9005
|
overlayPath,
|
|
8660
|
-
credentialsPath:
|
|
9006
|
+
credentialsPath: path12.join(home, ".credentials.yaml"),
|
|
8661
9007
|
args: passthrough,
|
|
8662
9008
|
env: io.env,
|
|
8663
9009
|
initialToken: token.access_token,
|
|
8664
9010
|
freshToken: async () => {
|
|
8665
9011
|
try {
|
|
8666
|
-
return (await getUserToken(source, 120)).access_token;
|
|
9012
|
+
return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
|
|
8667
9013
|
} catch (error) {
|
|
8668
9014
|
io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
|
|
8669
9015
|
return null;
|
|
@@ -8672,7 +9018,7 @@ ${agentsUsage}`);
|
|
|
8672
9018
|
});
|
|
8673
9019
|
}
|
|
8674
9020
|
async function status(args, io) {
|
|
8675
|
-
const { values } =
|
|
9021
|
+
const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
|
|
8676
9022
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8677
9023
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8678
9024
|
const claims = tokenClaims(token.access_token);
|
|
@@ -8686,35 +9032,25 @@ async function status(args, io) {
|
|
|
8686
9032
|
return ExitCode.Ok;
|
|
8687
9033
|
}
|
|
8688
9034
|
async function rotate(args, io) {
|
|
8689
|
-
const { values } =
|
|
9035
|
+
const { values } = parseArgs3({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
|
|
8690
9036
|
const interval = Number(values.interval);
|
|
8691
9037
|
if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
|
|
8692
9038
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8693
9039
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8694
|
-
const source =
|
|
9040
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8695
9041
|
const tokenFilePath = values.into ?? defaultTokenFilePath();
|
|
8696
9042
|
io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
|
|
8697
9043
|
return rotateTokenFile({
|
|
8698
9044
|
tokenFile: tokenFilePath,
|
|
8699
9045
|
intervalSeconds: interval,
|
|
8700
9046
|
once: values.once,
|
|
8701
|
-
freshToken: async () => (await getUserToken(source, Math.max(interval, 60) + 60)).access_token,
|
|
9047
|
+
freshToken: async () => (await getUserToken(source, { freshForSeconds: Math.max(interval, 60) + 60, organizationId: values["organization-id"] })).access_token,
|
|
8702
9048
|
report: (line) => io.stderr(line),
|
|
8703
9049
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8704
9050
|
});
|
|
8705
9051
|
}
|
|
8706
|
-
function agentsVerbUsage(verb) {
|
|
8707
|
-
const lines = agentsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `) || line.trimStart().startsWith(`${verb} `));
|
|
8708
|
-
return lines.length ? `diverge agents ${verb} ...
|
|
8709
|
-
|
|
8710
|
-
${lines.join("\n")}` : agentsUsage;
|
|
8711
|
-
}
|
|
8712
9052
|
async function runAgents(args, io) {
|
|
8713
9053
|
const [verb, ...rest] = args;
|
|
8714
|
-
if (verb && (rest.includes("--help") || rest.includes("-h")) && !rest.includes("--")) {
|
|
8715
|
-
io.stdout(agentsVerbUsage(verb));
|
|
8716
|
-
return ExitCode.Ok;
|
|
8717
|
-
}
|
|
8718
9054
|
switch (verb) {
|
|
8719
9055
|
case "setup":
|
|
8720
9056
|
return setup(rest, io);
|
|
@@ -8725,226 +9061,71 @@ async function runAgents(args, io) {
|
|
|
8725
9061
|
case "status":
|
|
8726
9062
|
return status(rest, io);
|
|
8727
9063
|
default:
|
|
8728
|
-
throw
|
|
8729
|
-
|
|
8730
|
-
${agentsUsage}`);
|
|
8731
|
-
}
|
|
8732
|
-
}
|
|
8733
|
-
|
|
8734
|
-
// src/commands/login.ts
|
|
8735
|
-
import { parseArgs as parseArgs3 } from "node:util";
|
|
8736
|
-
|
|
8737
|
-
// src/auth/organizations.ts
|
|
8738
|
-
function chooseOrganization(input) {
|
|
8739
|
-
const memberships = input.memberships;
|
|
8740
|
-
const flag = input.flag?.trim();
|
|
8741
|
-
if (flag) {
|
|
8742
|
-
const match = memberships.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
|
|
8743
|
-
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
|
|
8744
|
-
if (input.platformActor) return { kind: "chosen", organizationId: flag, reason: "flag" };
|
|
8745
|
-
throw new CliError(`Not one of your organizations: ${flag}. Yours: ${memberships.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
|
|
8746
|
-
}
|
|
8747
|
-
const remembered = input.remembered?.trim();
|
|
8748
|
-
if (remembered) {
|
|
8749
|
-
const match = memberships.find((organization) => organization.id === remembered);
|
|
8750
|
-
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
|
|
8751
|
-
if (input.platformActor) return { kind: "chosen", organizationId: remembered, reason: "remembered" };
|
|
8752
|
-
}
|
|
8753
|
-
if (memberships.length === 1) return { kind: "chosen", organizationId: memberships[0].id, name: memberships[0].name, reason: "only" };
|
|
8754
|
-
if (memberships.length === 0) return { kind: "none" };
|
|
8755
|
-
return { kind: "ask", options: memberships };
|
|
8756
|
-
}
|
|
8757
|
-
function organizationMenu(options) {
|
|
8758
|
-
return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
|
|
8759
|
-
}
|
|
8760
|
-
function pickFromMenu(answer, options) {
|
|
8761
|
-
const trimmed = answer.trim();
|
|
8762
|
-
const index = Number(trimmed);
|
|
8763
|
-
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
|
|
8764
|
-
const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
|
|
8765
|
-
if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
|
|
8766
|
-
return match;
|
|
8767
|
-
}
|
|
8768
|
-
|
|
8769
|
-
// src/auth/userinfo.ts
|
|
8770
|
-
async function fetchUserInfo(baseUrl, accessToken) {
|
|
8771
|
-
return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
|
|
8772
|
-
}
|
|
8773
|
-
async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
|
|
8774
|
-
try {
|
|
8775
|
-
const info = await fetchUserInfo(baseUrl, accessToken);
|
|
8776
|
-
return options.withSub ? `${info.email} (${info.sub})` : info.email;
|
|
8777
|
-
} catch (error) {
|
|
8778
|
-
return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
|
|
8779
|
-
}
|
|
8780
|
-
}
|
|
8781
|
-
|
|
8782
|
-
// src/prompt.ts
|
|
8783
|
-
import readline from "node:readline";
|
|
8784
|
-
function ask(question) {
|
|
8785
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8786
|
-
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
8787
|
-
rl.close();
|
|
8788
|
-
resolve(answer);
|
|
8789
|
-
}));
|
|
8790
|
-
}
|
|
8791
|
-
|
|
8792
|
-
// src/commands/login.ts
|
|
8793
|
-
var DEFAULT_SCOPES = "mcp:read mcp:execute inference:execute";
|
|
8794
|
-
var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
|
|
8795
|
-
|
|
8796
|
-
Sign in through the browser, then choose where to act. The browser only
|
|
8797
|
-
authenticates the person; the organization is chosen here afterwards \u2014 the
|
|
8798
|
-
one named by --organization-id (id or name), else the last login's, else the
|
|
8799
|
-
only membership, else a menu \u2014 and the login is bound to it on the platform.
|
|
8800
|
-
Switch later with: diverge org use <id|name>.
|
|
8801
|
-
--as <email> names the account: the platform signs that account in instead of
|
|
8802
|
-
reusing whatever session the browser holds, and issues nothing to any other.
|
|
8803
|
-
Without --as, the account of the last login is used; switching is explicit.
|
|
8804
|
-
Without --scopes, the scopes of the last login are asked for again; a first
|
|
8805
|
-
login asks for the CLI's own (mcp:read mcp:execute inference:execute).`;
|
|
8806
|
-
function resolveScopes(flag, config) {
|
|
8807
|
-
const explicit = flag?.trim();
|
|
8808
|
-
if (explicit) return explicit.split(/\s+/).join(" ");
|
|
8809
|
-
const remembered = config.scopes?.trim();
|
|
8810
|
-
return remembered ? remembered.split(/\s+/).join(" ") : DEFAULT_SCOPES;
|
|
8811
|
-
}
|
|
8812
|
-
function resolveLoginHint(flag, config) {
|
|
8813
|
-
const explicit = flag?.trim().toLowerCase();
|
|
8814
|
-
if (explicit) return explicit;
|
|
8815
|
-
const remembered = config.account?.trim().toLowerCase();
|
|
8816
|
-
return remembered || void 0;
|
|
8817
|
-
}
|
|
8818
|
-
async function runLogin(args, io) {
|
|
8819
|
-
const { values } = parseArgs3({
|
|
8820
|
-
args,
|
|
8821
|
-
options: {
|
|
8822
|
-
"organization-id": { type: "string" },
|
|
8823
|
-
scopes: { type: "string" },
|
|
8824
|
-
as: { type: "string" },
|
|
8825
|
-
"no-browser": { type: "boolean", default: false },
|
|
8826
|
-
"api-url": { type: "string" }
|
|
8827
|
-
},
|
|
8828
|
-
strict: true,
|
|
8829
|
-
allowPositionals: false
|
|
8830
|
-
});
|
|
8831
|
-
const config = readConfig(io.env);
|
|
8832
|
-
const apiUrl = resolveApiUrl(values["api-url"], io.env, config);
|
|
8833
|
-
if (values["api-url"]) writeConfig({ ...config, apiUrl }, io.env);
|
|
8834
|
-
const loginHint = resolveLoginHint(values.as, config);
|
|
8835
|
-
if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
|
|
8836
|
-
const scopes = resolveScopes(values.scopes, config);
|
|
8837
|
-
io.stdout(`Asking for scopes: ${scopes}${values.scopes ? "" : config.scopes ? " (the last login; pass --scopes to change)" : ""}.`);
|
|
8838
|
-
const token = await login({
|
|
8839
|
-
baseUrl: apiUrl,
|
|
8840
|
-
clientId: clientId(io.env),
|
|
8841
|
-
scopes,
|
|
8842
|
-
openBrowser: !values["no-browser"],
|
|
8843
|
-
tokenFile: tokenFile(io.env),
|
|
8844
|
-
loginHint,
|
|
8845
|
-
print: io.stdout
|
|
8846
|
-
});
|
|
8847
|
-
const claims = tokenClaims(token.access_token);
|
|
8848
|
-
let info;
|
|
8849
|
-
try {
|
|
8850
|
-
info = await fetchUserInfo(apiUrl, token.access_token);
|
|
8851
|
-
} catch (error) {
|
|
8852
|
-
throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
|
|
9064
|
+
throw unknownVerb("agents", agentsUsage, [], verb, rest);
|
|
8853
9065
|
}
|
|
8854
|
-
io.stdout(`Signed in as: ${info.email}`);
|
|
8855
|
-
const bound = await bindChosenOrganization(io, apiUrl, values["organization-id"], config.organization, info);
|
|
8856
|
-
const remaining = Math.max(0, Math.round(bound.token.expires_at - Date.now() / 1e3));
|
|
8857
|
-
io.stdout(`Logged in to ${apiUrl}; token cached (${remaining}s to expiry, auto-refreshes).`);
|
|
8858
|
-
writeConfig({ ...readConfig(io.env), account: info.email, scopes, organization: bound.organizationId }, io.env);
|
|
8859
|
-
const granted = new Set(tokenClaims(bound.token.access_token).scopes ?? []);
|
|
8860
|
-
const missing = scopes.split(" ").filter((scope) => scope && !granted.has(scope));
|
|
8861
|
-
if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
|
|
8862
|
-
return 0;
|
|
8863
|
-
}
|
|
8864
|
-
async function bindChosenOrganization(io, apiUrl, flag, remembered, info) {
|
|
8865
|
-
const memberships = info.organizations ?? [];
|
|
8866
|
-
let choice = chooseOrganization({ flag, remembered, memberships, platformActor: info.platformActor });
|
|
8867
|
-
if (choice.kind === "none") {
|
|
8868
|
-
throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
|
|
8869
|
-
}
|
|
8870
|
-
if (choice.kind === "ask") {
|
|
8871
|
-
if (!io.interactive) {
|
|
8872
|
-
throw new CliError(`You belong to several organizations; pass --organization-id <id|name>:
|
|
8873
|
-
${organizationMenu(choice.options).join("\n")}`);
|
|
8874
|
-
}
|
|
8875
|
-
io.stdout("Choose the organization to act in:");
|
|
8876
|
-
for (const line of organizationMenu(choice.options)) io.stdout(line);
|
|
8877
|
-
const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
|
|
8878
|
-
choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
|
|
8879
|
-
}
|
|
8880
|
-
const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
|
|
8881
|
-
const token = await bindOrganization(source, choice.organizationId);
|
|
8882
|
-
const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
|
|
8883
|
-
const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
|
|
8884
|
-
io.stdout(`Organization: ${label}${why}`);
|
|
8885
|
-
return { organizationId: choice.organizationId, token };
|
|
8886
9066
|
}
|
|
8887
9067
|
|
|
8888
9068
|
// src/commands/org.ts
|
|
8889
9069
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
8890
9070
|
var orgUsage = `diverge org [use <id|name>]
|
|
8891
9071
|
|
|
8892
|
-
|
|
8893
|
-
use <id|name
|
|
8894
|
-
|
|
8895
|
-
|
|
9072
|
+
(no verb) list the organizations you may act in, the default marked
|
|
9073
|
+
use <id|name> make it the default: where every command that names no organization acts,
|
|
9074
|
+
in every shell on this machine that shares the login
|
|
9075
|
+
|
|
9076
|
+
The default is a local preference: the platform is asked only for a token
|
|
9077
|
+
there, which proves you may act in it. For one command, pass
|
|
9078
|
+
--organization-id <id> (or set DIVERGENT_ORG_ID) instead. A platform actor may
|
|
9079
|
+
act in, and so sees, every organization.`;
|
|
8896
9080
|
async function runOrg(args, io) {
|
|
8897
9081
|
const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
|
|
8898
|
-
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8899
|
-
const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
|
|
8900
|
-
const token = await getUserToken(source);
|
|
8901
|
-
const info = await fetchUserInfo(apiUrl, token.access_token);
|
|
8902
|
-
const memberships = info.organizations ?? [];
|
|
8903
|
-
const current = tokenClaims(token.access_token).organizationId ?? void 0;
|
|
8904
9082
|
const verb = positionals[0];
|
|
9083
|
+
if (verb && verb !== "use") throw unknownVerb("org", orgUsage, [], verb, positionals.slice(1));
|
|
9084
|
+
const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
|
|
9085
|
+
const info = await fetchUserInfo(source.baseUrl, (await getUserToken(source)).access_token);
|
|
9086
|
+
const organizations = info.organizations ?? [];
|
|
9087
|
+
const current = requireLogin(source).organization;
|
|
8905
9088
|
if (!verb) {
|
|
8906
|
-
if (
|
|
9089
|
+
if (organizations.length === 0) {
|
|
8907
9090
|
io.stdout("You are not a member of any organization.");
|
|
8908
9091
|
return 0;
|
|
8909
9092
|
}
|
|
8910
|
-
for (const organization of
|
|
8911
|
-
if (current && !memberships.some((organization) => organization.id === current)) io.stdout(`* ${current} (not a membership; acting as the platform)`);
|
|
9093
|
+
for (const organization of organizations) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
|
|
8912
9094
|
return 0;
|
|
8913
9095
|
}
|
|
8914
|
-
if (verb !== "use") throw new CliError(`Unknown verb: ${verb}
|
|
8915
|
-
|
|
8916
|
-
${orgUsage}`);
|
|
8917
9096
|
const target = positionals[1];
|
|
8918
9097
|
if (!target) throw new CliError("org use needs an organization id or name");
|
|
8919
|
-
const choice = chooseOrganization({ flag: target,
|
|
9098
|
+
const choice = chooseOrganization({ flag: target, organizations });
|
|
8920
9099
|
if (choice.kind !== "chosen") throw new CliError("org use needs an organization id or name");
|
|
8921
|
-
const
|
|
8922
|
-
|
|
8923
|
-
io.stdout(`Organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(bound.access_token).scopes ?? []).join(" ") || "none"}`);
|
|
9100
|
+
const token = await useOrganization(source, choice.organizationId);
|
|
9101
|
+
io.stdout(`Default organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(token.access_token).scopes ?? []).join(" ") || "none"}`);
|
|
8924
9102
|
return 0;
|
|
8925
9103
|
}
|
|
8926
9104
|
|
|
8927
9105
|
// src/commands/logout.ts
|
|
8928
|
-
var logoutUsage = "diverge logout\n\nRevoke
|
|
9106
|
+
var logoutUsage = "diverge logout\n\nRevoke this deployment's login on the platform and forget it locally, with every token issued from it. Logins to other deployments are kept.";
|
|
8929
9107
|
async function runLogout(_args, io) {
|
|
8930
|
-
const
|
|
8931
|
-
const token =
|
|
9108
|
+
const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
|
|
9109
|
+
const token = loadLogin(source);
|
|
8932
9110
|
if (!token) {
|
|
8933
|
-
io.stdout(
|
|
9111
|
+
io.stdout(`Not logged in to ${source.baseUrl}.`);
|
|
8934
9112
|
return 0;
|
|
8935
9113
|
}
|
|
8936
9114
|
if (token.refresh_token) {
|
|
8937
9115
|
try {
|
|
8938
|
-
await revokeRefreshToken(
|
|
9116
|
+
await revokeRefreshToken(source.baseUrl, source.clientId, token.refresh_token);
|
|
8939
9117
|
} catch (error) {
|
|
8940
|
-
io.stderr(`warning: could not revoke the session on ${
|
|
9118
|
+
io.stderr(`warning: could not revoke the session on ${source.baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
8941
9119
|
}
|
|
8942
9120
|
}
|
|
8943
|
-
|
|
8944
|
-
io.stdout(`Logged out of ${
|
|
9121
|
+
forgetLogin(source);
|
|
9122
|
+
io.stdout(`Logged out of ${source.baseUrl}.`);
|
|
8945
9123
|
return 0;
|
|
8946
9124
|
}
|
|
8947
9125
|
|
|
9126
|
+
// src/commands/status.ts
|
|
9127
|
+
import path13 from "node:path";
|
|
9128
|
+
|
|
8948
9129
|
// src/tools/settings.ts
|
|
8949
9130
|
var BRIDGE_SCOPES = "mcp:read mcp:execute";
|
|
8950
9131
|
var LOGIN_HINT = "diverge login";
|
|
@@ -8952,21 +9133,19 @@ function loadToolsSettings(options, env = process.env) {
|
|
|
8952
9133
|
const apiUrl = resolveApiUrl(void 0, env);
|
|
8953
9134
|
const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
|
|
8954
9135
|
let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
|
|
8955
|
-
const cached =
|
|
8956
|
-
if (cached
|
|
8957
|
-
organizationId ||=
|
|
8958
|
-
if (!organizationId) throw new CliError(`The login
|
|
9136
|
+
const cached = loadLogin(tokenSource(env, apiUrl));
|
|
9137
|
+
if (cached) {
|
|
9138
|
+
organizationId ||= cached.organization ?? "";
|
|
9139
|
+
if (!organizationId) throw new CliError(`The login has no default organization. Pass --organization-id or choose one: diverge org use <id|name>`);
|
|
8959
9140
|
return { apiUrl, organizationId, projectId, mode: "operator", clientId: clientId(env) };
|
|
8960
9141
|
}
|
|
8961
|
-
if (!organizationId) throw new CliError("No organization configured. Pass --organization-id or set DIVERGENT_ORG_ID.");
|
|
8962
9142
|
const serviceClientId = env.DIVERGENT_CLIENT_ID?.trim() ?? "";
|
|
8963
9143
|
const serviceClientSecret = env.DIVERGENT_CLIENT_SECRET?.trim() ?? "";
|
|
8964
9144
|
const delegatedUserId = env.DIVERGENT_USER_ID?.trim() ?? "";
|
|
8965
9145
|
if (!serviceClientId || !serviceClientSecret) {
|
|
8966
|
-
throw
|
|
8967
|
-
Sign in as yourself: ${LOGIN_HINT}
|
|
8968
|
-
Or set DIVERGENT_CLIENT_ID and DIVERGENT_CLIENT_SECRET.`);
|
|
9146
|
+
throw noLogin(apiUrl, "Or, unattended, set DIVERGENT_CLIENT_ID, DIVERGENT_CLIENT_SECRET, DIVERGENT_USER_ID and DIVERGENT_ORG_ID.");
|
|
8969
9147
|
}
|
|
9148
|
+
if (!organizationId) throw new CliError("Service credentials act in the organization named: pass --organization-id or set DIVERGENT_ORG_ID.");
|
|
8970
9149
|
if (!delegatedUserId) {
|
|
8971
9150
|
throw new CliError(`DIVERGENT_USER_ID is required for service-credential execution: tools run under that human's RBAC, and without it the service account executes app-only, which governed tools deliberately restrict.
|
|
8972
9151
|
Signing in as yourself avoids this entirely: ${LOGIN_HINT}`);
|
|
@@ -8984,10 +9163,9 @@ function actingAsLine(settings, env = process.env) {
|
|
|
8984
9163
|
actingAs ??= (async () => {
|
|
8985
9164
|
let accessToken;
|
|
8986
9165
|
try {
|
|
8987
|
-
accessToken = (await getUserToken(
|
|
9166
|
+
accessToken = (await getUserToken(tokenSource(env, settings.apiUrl))).access_token;
|
|
8988
9167
|
} catch (error) {
|
|
8989
|
-
|
|
8990
|
-
return `Acting as: ${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
|
|
9168
|
+
return `Acting as: unknown user (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
|
|
8991
9169
|
}
|
|
8992
9170
|
return `Acting as: ${await describeAccount(settings.apiUrl, accessToken, tokenClaims(accessToken).sub)}`;
|
|
8993
9171
|
})();
|
|
@@ -9007,54 +9185,73 @@ function describeToolExecution(env = process.env) {
|
|
|
9007
9185
|
var statusUsage = "diverge status\n\nShow the deployment, the cached login, whose account it is, what it carries, and who a tool call would run as. One request: the account behind the login.";
|
|
9008
9186
|
async function runStatus(_args, io) {
|
|
9009
9187
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
9188
|
+
const source = tokenSource(io.env, apiUrl);
|
|
9010
9189
|
io.stdout(`diverge ${VERSION}`);
|
|
9011
9190
|
io.stdout(`Deployment: ${apiUrl}`);
|
|
9012
|
-
io.stdout(`State: ${
|
|
9013
|
-
const
|
|
9014
|
-
if (!
|
|
9015
|
-
io.stdout(
|
|
9191
|
+
io.stdout(`State: ${path13.dirname(source.loginFile)}`);
|
|
9192
|
+
const record = loadLogin(source);
|
|
9193
|
+
if (!record) {
|
|
9194
|
+
io.stdout(`Not logged in. Run: diverge login${apiUrl === DEFAULT_API_URL ? "" : ` --api-url ${apiUrl}`}`);
|
|
9016
9195
|
io.stdout(describeToolExecution(io.env));
|
|
9017
9196
|
return 1;
|
|
9018
9197
|
}
|
|
9019
|
-
|
|
9020
|
-
const
|
|
9021
|
-
const mismatch = token.base_url !== apiUrl ? ` (for ${token.base_url}, not the configured deployment)` : "";
|
|
9022
|
-
io.stdout(`Login: ${isFresh(token, 0) ? `valid for ${remaining}s` : "expired (auto-refreshes)"}; refresh token ${token.refresh_token ? "present" : "absent"}${mismatch}`);
|
|
9023
|
-
const account = await signedInAs(token, io);
|
|
9198
|
+
io.stdout(`Login: ${describeSignIn(record)}`);
|
|
9199
|
+
const account = await signedInAs(source);
|
|
9024
9200
|
io.stdout(`Signed in as: ${account.line}`);
|
|
9025
|
-
const organization = account.organizations?.find((candidate) => candidate.id ===
|
|
9026
|
-
io.stdout(`Organization: ${
|
|
9027
|
-
io.stdout(`Scopes: ${
|
|
9201
|
+
const organization = account.organizations?.find((candidate) => candidate.id === record.organization);
|
|
9202
|
+
io.stdout(`Organization: ${record.organization ? `${organization ? `${organization.name} (${record.organization})` : record.organization} (the default; commands may name another)` : "none (run: diverge org use <id|name>)"}`);
|
|
9203
|
+
io.stdout(`Scopes: ${account.scopes ?? "unknown"}`);
|
|
9028
9204
|
io.stdout(describeToolExecution(io.env));
|
|
9029
9205
|
return 0;
|
|
9030
9206
|
}
|
|
9031
|
-
async function signedInAs(
|
|
9032
|
-
const sub = tokenClaims(cached.access_token).sub;
|
|
9207
|
+
async function signedInAs(source) {
|
|
9033
9208
|
let accessToken;
|
|
9034
9209
|
try {
|
|
9035
|
-
accessToken = (await getUserToken(
|
|
9210
|
+
accessToken = (await getUserToken(source)).access_token;
|
|
9036
9211
|
} catch (error) {
|
|
9037
|
-
return { line:
|
|
9212
|
+
return { line: `unknown (${error instanceof Error ? error.message : String(error)})` };
|
|
9038
9213
|
}
|
|
9214
|
+
const claims = tokenClaims(accessToken);
|
|
9215
|
+
const scopes = (claims.scopes ?? []).join(" ") || "none";
|
|
9039
9216
|
try {
|
|
9040
|
-
const info = await fetchUserInfo(
|
|
9041
|
-
return { line: `${info.email} (${info.sub})`, organizations: info.organizations };
|
|
9217
|
+
const info = await fetchUserInfo(source.baseUrl, accessToken);
|
|
9218
|
+
return { line: `${info.email} (${info.sub})`, organizations: info.organizations, scopes };
|
|
9042
9219
|
} catch (error) {
|
|
9043
|
-
return { line: `${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})
|
|
9220
|
+
return { line: `${claims.sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`, scopes };
|
|
9044
9221
|
}
|
|
9045
9222
|
}
|
|
9223
|
+
function describeSignIn(record, nowMs = Date.now()) {
|
|
9224
|
+
const { idle, cap } = loginExpiry(record);
|
|
9225
|
+
if (idle === null) return "refresh token present";
|
|
9226
|
+
const at = (seconds) => new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
9227
|
+
const left = (seconds) => {
|
|
9228
|
+
const remaining = seconds - nowMs / 1e3;
|
|
9229
|
+
const hours = Math.floor(remaining / 3600);
|
|
9230
|
+
return hours >= 24 ? `${Math.floor(hours / 24)}d ${hours % 24}h` : `${hours}h ${Math.floor(remaining % 3600 / 60)}m`;
|
|
9231
|
+
};
|
|
9232
|
+
if (idle <= nowMs / 1e3) return `sign-in expired at ${at(idle)} (run: diverge login)`;
|
|
9233
|
+
const idleLine = `renews while used; idle until ${at(idle)} (${left(idle)} left)`;
|
|
9234
|
+
if (cap === null || cap <= idle) return cap === null ? idleLine : `ends ${at(idle)} (${left(idle)} left; 30-day limit, run: diverge login then)`;
|
|
9235
|
+
return `${idleLine}; ends at the latest ${at(cap)} (${left(cap)} left)`;
|
|
9236
|
+
}
|
|
9046
9237
|
|
|
9047
9238
|
// src/commands/token.ts
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9239
|
+
import { parseArgs as parseArgs5 } from "node:util";
|
|
9240
|
+
var tokenUsage = `diverge token [--organization-id <id>]
|
|
9241
|
+
|
|
9242
|
+
Print a fresh access token and nothing else: the credential helper for Claude
|
|
9243
|
+
Code, Codex and forge. The token is for the organization named by
|
|
9244
|
+
--organization-id or DIVERGENT_ORG_ID, else the login's default.`;
|
|
9245
|
+
async function runToken(args, io) {
|
|
9246
|
+
const { values } = parseArgs5({ args, options: { "organization-id": { type: "string" } }, strict: true, allowPositionals: false });
|
|
9247
|
+
const organizationId = values["organization-id"]?.trim() || io.env.DIVERGENT_ORG_ID?.trim() || void 0;
|
|
9248
|
+
const token = await getUserToken(tokenSource(io.env, resolveApiUrl(void 0, io.env)), { organizationId });
|
|
9052
9249
|
io.stdout(token.access_token);
|
|
9053
9250
|
return 0;
|
|
9054
9251
|
}
|
|
9055
9252
|
|
|
9056
9253
|
// src/commands/tools.ts
|
|
9057
|
-
import { parseArgs as
|
|
9254
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
9058
9255
|
|
|
9059
9256
|
// src/mcp/bridge.ts
|
|
9060
9257
|
import readline2 from "node:readline";
|
|
@@ -9078,26 +9275,26 @@ var ToolsClient = class {
|
|
|
9078
9275
|
serviceToken = null;
|
|
9079
9276
|
async token() {
|
|
9080
9277
|
if (this.settings.mode === "operator") {
|
|
9081
|
-
|
|
9082
|
-
return (await getUserToken({ tokenFile: tokenFile(this.env), baseUrl: this.settings.apiUrl, clientId: this.settings.clientId })).access_token;
|
|
9083
|
-
} catch (error) {
|
|
9084
|
-
throw new CliError(`${error instanceof Error ? error.message : String(error)}
|
|
9085
|
-
Sign in with: ${LOGIN_HINT}`);
|
|
9086
|
-
}
|
|
9278
|
+
return (await getUserToken(tokenSource(this.env, this.settings.apiUrl), { organizationId: this.settings.organizationId })).access_token;
|
|
9087
9279
|
}
|
|
9088
9280
|
if (this.serviceToken && Date.now() < this.serviceToken.expiresAtMs) return this.serviceToken.value;
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9281
|
+
let response;
|
|
9282
|
+
try {
|
|
9283
|
+
response = await this.fetchImpl(tokenEndpoint(this.settings.apiUrl), {
|
|
9284
|
+
method: "POST",
|
|
9285
|
+
headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
|
|
9286
|
+
body: JSON.stringify({
|
|
9287
|
+
grant_type: "client_credentials",
|
|
9288
|
+
client_id: this.settings.serviceClientId,
|
|
9289
|
+
client_secret: this.settings.serviceClientSecret,
|
|
9290
|
+
scopes: BRIDGE_SCOPES,
|
|
9291
|
+
organization_id: this.settings.organizationId
|
|
9292
|
+
}),
|
|
9293
|
+
signal: AbortSignal.timeout(3e4)
|
|
9294
|
+
});
|
|
9295
|
+
} catch (error) {
|
|
9296
|
+
throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9297
|
+
}
|
|
9101
9298
|
if (!response.ok) throw new CliError(`Token acquisition failed (${response.status}): ${(await response.text()).slice(0, 400)}`);
|
|
9102
9299
|
const body = await response.json();
|
|
9103
9300
|
if (!body.access_token) throw new CliError("Token response contained no access_token");
|
|
@@ -9110,10 +9307,10 @@ var ToolsClient = class {
|
|
|
9110
9307
|
if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
|
|
9111
9308
|
return headers;
|
|
9112
9309
|
}
|
|
9113
|
-
scoped(
|
|
9310
|
+
scoped(path15) {
|
|
9114
9311
|
const query = new URLSearchParams({ organizationId: this.settings.organizationId });
|
|
9115
9312
|
if (this.settings.projectId) query.set("projectId", this.settings.projectId);
|
|
9116
|
-
return `${this.settings.apiUrl}${
|
|
9313
|
+
return `${this.settings.apiUrl}${path15}?${query.toString()}`;
|
|
9117
9314
|
}
|
|
9118
9315
|
async send(method, url, body) {
|
|
9119
9316
|
return this.request(method, url, body, void 0);
|
|
@@ -9125,9 +9322,10 @@ var ToolsClient = class {
|
|
|
9125
9322
|
*/
|
|
9126
9323
|
async request(method, url, body, refused) {
|
|
9127
9324
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
9325
|
+
const headers = await this.headers();
|
|
9128
9326
|
let response;
|
|
9129
9327
|
try {
|
|
9130
|
-
response = await this.fetchImpl(url, { method, headers
|
|
9328
|
+
response = await this.fetchImpl(url, { method, headers, body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
9131
9329
|
} catch (error) {
|
|
9132
9330
|
throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9133
9331
|
}
|
|
@@ -9159,8 +9357,32 @@ var ToolsClient = class {
|
|
|
9159
9357
|
const body = await this.send("GET", this.scoped("/api/plugins/functions/tools"));
|
|
9160
9358
|
return body.tools ?? [];
|
|
9161
9359
|
}
|
|
9162
|
-
|
|
9163
|
-
|
|
9360
|
+
/**
|
|
9361
|
+
* One tool, looked up at its source when the catalog does not list it yet —
|
|
9362
|
+
* a tool deployed after its server's catalog was read. A 404 is an answer.
|
|
9363
|
+
*/
|
|
9364
|
+
describeTool(name) {
|
|
9365
|
+
return this.request("GET", this.scoped(`/api/plugins/functions/tools/${encodeURIComponent(name)}`), void 0, (status2, text) => {
|
|
9366
|
+
if (status2 !== 404) return void 0;
|
|
9367
|
+
try {
|
|
9368
|
+
const parsed = JSON.parse(text);
|
|
9369
|
+
if (typeof parsed.error === "string" && parsed.error) return { notFound: parsed.error };
|
|
9370
|
+
} catch {
|
|
9371
|
+
}
|
|
9372
|
+
return { notFound: `No tool named '${name}' is available.` };
|
|
9373
|
+
});
|
|
9374
|
+
}
|
|
9375
|
+
/**
|
|
9376
|
+
* One call. The platform answers a completed call with its invocation
|
|
9377
|
+
* envelope, `{status: 'succeeded', invocationId?, result}`; the tool's own
|
|
9378
|
+
* payload is `result` inside it. Every caller wants the payload, so it is
|
|
9379
|
+
* unwrapped here, once — a pending approval passes through as it is.
|
|
9380
|
+
*/
|
|
9381
|
+
async callTool(name, args) {
|
|
9382
|
+
const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
|
|
9383
|
+
const outcome = body.result;
|
|
9384
|
+
if (outcome?.status !== "succeeded") return body;
|
|
9385
|
+
return { ...body, result: outcome.result };
|
|
9164
9386
|
}
|
|
9165
9387
|
/** The read half of raise-and-read (ADR 0047); anyone but the raiser sees a 404. */
|
|
9166
9388
|
getInvocation(invocationId) {
|
|
@@ -9382,7 +9604,7 @@ async function waitForInvocation(id, options, deps) {
|
|
|
9382
9604
|
|
|
9383
9605
|
// src/tools/call-args.ts
|
|
9384
9606
|
import fs9 from "node:fs";
|
|
9385
|
-
function resolveCallArgs(options,
|
|
9607
|
+
async function resolveCallArgs(options, stdin = readStdin) {
|
|
9386
9608
|
if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
|
|
9387
9609
|
throw new CliError("give the arguments once: --args-json or --args-file");
|
|
9388
9610
|
}
|
|
@@ -9397,7 +9619,7 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
|
|
|
9397
9619
|
}
|
|
9398
9620
|
} else if (options.argsJson === "-") {
|
|
9399
9621
|
source = "--args-json - (stdin)";
|
|
9400
|
-
text =
|
|
9622
|
+
text = await stdin();
|
|
9401
9623
|
} else {
|
|
9402
9624
|
source = "--args-json";
|
|
9403
9625
|
text = options.argsJson ?? "{}";
|
|
@@ -9411,6 +9633,11 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
|
|
|
9411
9633
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(`${source} must be a JSON object`);
|
|
9412
9634
|
return parsed;
|
|
9413
9635
|
}
|
|
9636
|
+
async function readStdin() {
|
|
9637
|
+
const chunks = [];
|
|
9638
|
+
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
9639
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
9640
|
+
}
|
|
9414
9641
|
|
|
9415
9642
|
// src/tools/batch.ts
|
|
9416
9643
|
function parseBatch(text) {
|
|
@@ -9445,21 +9672,29 @@ async function runOne(entry, deps) {
|
|
|
9445
9672
|
if (body.success === false) return { tool: entry.tool, status: "failed", error: body.error || "Tool execution failed" };
|
|
9446
9673
|
if (!isPending(body.result)) return { tool: entry.tool, status: "executed", result: body.result ?? null };
|
|
9447
9674
|
const pending = { tool: entry.tool, status: "pending", invocationId: body.result.invocationId, decideAt: deps.decideAt(body.result) };
|
|
9675
|
+
deps.onPending?.(body.result);
|
|
9448
9676
|
if (!deps.waitFor) return pending;
|
|
9449
9677
|
const settled = await deps.waitFor(body.result);
|
|
9450
|
-
|
|
9678
|
+
return (settled && outcomeOf(entry.tool, pending.invocationId, settled)) ?? pending;
|
|
9679
|
+
}
|
|
9680
|
+
function outcomeOf(tool, invocationId, invocation) {
|
|
9681
|
+
switch (invocation.status) {
|
|
9451
9682
|
case "succeeded":
|
|
9452
|
-
return { tool
|
|
9683
|
+
return { tool, status: "executed", result: invocation.result ?? null, invocationId };
|
|
9453
9684
|
case "rejected":
|
|
9454
|
-
return { tool
|
|
9685
|
+
return { tool, status: "rejected", invocationId, error: invocation.decisionReason ?? void 0 };
|
|
9455
9686
|
case "expired":
|
|
9456
|
-
return { tool
|
|
9687
|
+
return { tool, status: "expired", invocationId };
|
|
9457
9688
|
case "failed":
|
|
9458
|
-
return { tool
|
|
9689
|
+
return { tool, status: "failed", invocationId, error: invocation.errorMessage || "execution failed" };
|
|
9459
9690
|
default:
|
|
9460
|
-
return
|
|
9691
|
+
return null;
|
|
9461
9692
|
}
|
|
9462
9693
|
}
|
|
9694
|
+
function readBackOutcomeOf(tool, invocationId, invocation) {
|
|
9695
|
+
if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) return { tool, status: "pending", invocationId };
|
|
9696
|
+
return outcomeOf(tool, invocationId, invocation);
|
|
9697
|
+
}
|
|
9463
9698
|
function worstExitCode(outcomes) {
|
|
9464
9699
|
const has = (status2) => outcomes.some((outcome) => outcome.status === status2);
|
|
9465
9700
|
if (has("failed")) return ExitCode.Failure;
|
|
@@ -9478,6 +9713,10 @@ function matchesCapability(selector, capability) {
|
|
|
9478
9713
|
if (selector === capability) return true;
|
|
9479
9714
|
return selector.endsWith("*") && capability.startsWith(selector.slice(0, -1));
|
|
9480
9715
|
}
|
|
9716
|
+
function matchesFilter(filter, tool) {
|
|
9717
|
+
const needle = filter.toLowerCase();
|
|
9718
|
+
return tool.name.toLowerCase().includes(needle) || (tool.description ?? "").toLowerCase().includes(needle);
|
|
9719
|
+
}
|
|
9481
9720
|
function formatToolList(tools) {
|
|
9482
9721
|
if (tools.length === 0) return ["No tools available."];
|
|
9483
9722
|
const nameWidth = Math.max(...tools.map((tool) => tool.name.length));
|
|
@@ -9536,38 +9775,29 @@ function formatPending(result, apiUrl, organizationId) {
|
|
|
9536
9775
|
lines.push(`Read the outcome later: diverge tools invocation ${result.invocationId} --organization-id ${organizationId}`);
|
|
9537
9776
|
return lines;
|
|
9538
9777
|
}
|
|
9539
|
-
function settledOutcome(invocation) {
|
|
9540
|
-
switch (invocation.status) {
|
|
9541
|
-
case "succeeded":
|
|
9542
|
-
return { stdout: JSON.stringify(invocation.result ?? null, null, 2), exitCode: ExitCode.Ok };
|
|
9543
|
-
case "rejected":
|
|
9544
|
-
return { stderr: `Rejected by the operator${invocation.decisionReason ? `: ${invocation.decisionReason}` : "."}`, exitCode: ExitCode.Rejected };
|
|
9545
|
-
case "expired":
|
|
9546
|
-
return { stderr: "Expired before anyone decided. Raise a fresh call; the old id is final.", exitCode: ExitCode.Expired };
|
|
9547
|
-
case "failed":
|
|
9548
|
-
throw new CliError(`Approved, but execution failed: ${invocation.errorMessage || "unknown error"}`);
|
|
9549
|
-
default:
|
|
9550
|
-
throw new CliError(`Unexpected invocation status: ${invocation.status}`);
|
|
9551
|
-
}
|
|
9552
|
-
}
|
|
9553
9778
|
|
|
9554
9779
|
// src/commands/tools.ts
|
|
9555
9780
|
var toolsUsage = `diverge tools <list|capabilities|describe|call|run|invocation|bridge> [--organization-id <id>] [--project-id <id>] ...
|
|
9556
9781
|
|
|
9557
|
-
list [--filter <
|
|
9782
|
+
list [--filter <text>] [--capability <selector>] [--json]
|
|
9783
|
+
--filter searches tool names and descriptions, ignoring case
|
|
9558
9784
|
capabilities [--json]
|
|
9559
9785
|
describe <name> [--json]
|
|
9560
|
-
call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>]
|
|
9786
|
+
call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>] [--json]
|
|
9561
9787
|
arguments inline, from stdin (-), or from a file: PowerShell strips quotes inside inline JSON
|
|
9562
|
-
|
|
9788
|
+
--json prints exactly one JSON object on stdout, errors included:
|
|
9789
|
+
{tool, status: executed|pending|rejected|expired|failed, result?, error?, invocationId?, decideAt?}
|
|
9790
|
+
run <file|-> [--wait] [--timeout <seconds>]
|
|
9563
9791
|
several calls from one file (a JSON array of {tool, args}, or one per line), in order,
|
|
9564
9792
|
one JSON line of outcome each; exit code is the worst: 1 failed, 4 rejected, 5 expired, 3 pending
|
|
9565
9793
|
invocation <id> [--wait] [--timeout <seconds>] [--json]
|
|
9794
|
+
--json prints the invocation record; the exit code is call's for the same status
|
|
9566
9795
|
bridge [--tools <name,name>] (stdio MCP server; the allowlist keeps a harness to the tools it was given)
|
|
9567
9796
|
|
|
9568
9797
|
Discover and execute platform tools as yourself. A write that needs a human is
|
|
9569
9798
|
persisted as a pending invocation, decided in Compass, and read back here by
|
|
9570
|
-
id. Exit codes
|
|
9799
|
+
id. Exit codes, the same for call, run and invocation, --json or not:
|
|
9800
|
+
0 executed, 1 failed, 3 pending, 4 rejected, 5 expired.`;
|
|
9571
9801
|
var SCOPE_OPTIONS = { "organization-id": { type: "string" }, "project-id": { type: "string" } };
|
|
9572
9802
|
async function client(values, io) {
|
|
9573
9803
|
const settings = loadToolsSettings({ organizationId: values["organization-id"], projectId: values["project-id"] }, io.env);
|
|
@@ -9575,39 +9805,79 @@ async function client(values, io) {
|
|
|
9575
9805
|
io.stderr(await actingAsLine(settings, io.env));
|
|
9576
9806
|
return { settings, client: new ToolsClient(settings, io.env) };
|
|
9577
9807
|
}
|
|
9578
|
-
function emit(io, outcome) {
|
|
9579
|
-
if (outcome.stdout !== void 0) io.stdout(outcome.stdout);
|
|
9580
|
-
if (outcome.stderr !== void 0) io.stderr(outcome.stderr);
|
|
9581
|
-
return outcome.exitCode;
|
|
9582
|
-
}
|
|
9583
9808
|
var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9809
|
+
var CALL_OPTIONS = { ...SCOPE_OPTIONS, "args-json": { type: "string" }, "args-file": { type: "string" }, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } };
|
|
9810
|
+
function timeoutOf(seconds) {
|
|
9811
|
+
const timeoutMs = Number(seconds) * 1e3;
|
|
9812
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
|
|
9813
|
+
return timeoutMs;
|
|
9814
|
+
}
|
|
9815
|
+
function waiter(tools, timeoutMs) {
|
|
9816
|
+
return async (pending) => {
|
|
9817
|
+
const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
|
|
9818
|
+
return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
|
|
9819
|
+
};
|
|
9820
|
+
}
|
|
9821
|
+
async function call(rest, io) {
|
|
9822
|
+
const json = rest.includes("--json");
|
|
9823
|
+
let tool = "";
|
|
9824
|
+
let waited = "";
|
|
9825
|
+
let outcome;
|
|
9826
|
+
try {
|
|
9827
|
+
const { values, positionals } = parseArgs6({ args: rest, options: CALL_OPTIONS, strict: true, allowPositionals: true });
|
|
9828
|
+
tool = positionals[0] ?? "";
|
|
9829
|
+
if (!tool) throw new CliError("call needs a tool name");
|
|
9830
|
+
const args = await resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
|
|
9831
|
+
const timeoutMs = timeoutOf(values.timeout);
|
|
9832
|
+
if (values.wait) waited = values.timeout;
|
|
9833
|
+
const { settings, client: tools } = await client(values, io);
|
|
9834
|
+
outcome = await runOne({ tool, args }, {
|
|
9835
|
+
callTool: (name, callArgs) => tools.callTool(name, callArgs),
|
|
9836
|
+
decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
|
|
9837
|
+
...json ? {} : { onPending: (pending) => formatPending(pending, settings.apiUrl, settings.organizationId).forEach((line) => io.stdout(line)) },
|
|
9838
|
+
...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
|
|
9839
|
+
});
|
|
9840
|
+
} catch (error) {
|
|
9841
|
+
if (!json) throw error;
|
|
9842
|
+
outcome = { tool, status: "failed", error: unknownOption(error, toolsUsage, ["call"]) ?? (error instanceof Error ? error.message : String(error)) };
|
|
9843
|
+
}
|
|
9844
|
+
if (json) io.stdout(JSON.stringify(outcome, null, 2));
|
|
9845
|
+
else printOutcome(outcome, waited, io);
|
|
9846
|
+
return worstExitCode([outcome]);
|
|
9847
|
+
}
|
|
9848
|
+
function printOutcome(outcome, waited, io) {
|
|
9849
|
+
switch (outcome.status) {
|
|
9850
|
+
case "executed":
|
|
9851
|
+
io.stdout(JSON.stringify(outcome.result ?? null, null, 2));
|
|
9852
|
+
return;
|
|
9853
|
+
case "pending":
|
|
9854
|
+
if (waited) io.stderr(`Still waiting on the operator after ${waited}s; the invocation remains open.`);
|
|
9855
|
+
return;
|
|
9856
|
+
case "rejected":
|
|
9857
|
+
io.stderr(`Rejected by the operator${outcome.error ? `: ${outcome.error}` : "."}`);
|
|
9858
|
+
return;
|
|
9859
|
+
case "expired":
|
|
9860
|
+
io.stderr("Expired before anyone decided. Raise a fresh call; the old id is final.");
|
|
9861
|
+
return;
|
|
9862
|
+
case "failed":
|
|
9863
|
+
throw new CliError(outcome.invocationId ? `Approved, but execution failed: ${outcome.error}` : outcome.error || "Tool execution failed");
|
|
9864
|
+
}
|
|
9591
9865
|
}
|
|
9592
9866
|
async function runTools(args, io) {
|
|
9593
9867
|
const [verb, ...rest] = args;
|
|
9594
|
-
if (verb && (rest.includes("--help") || rest.includes("-h"))) {
|
|
9595
|
-
io.stdout(toolsVerbUsage(verb));
|
|
9596
|
-
return ExitCode.Ok;
|
|
9597
|
-
}
|
|
9598
9868
|
switch (verb) {
|
|
9599
9869
|
case "list": {
|
|
9600
|
-
const { values } =
|
|
9870
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
|
|
9601
9871
|
const { client: tools } = await client(values, io);
|
|
9602
9872
|
let list = await tools.listTools();
|
|
9603
|
-
if (values.filter) list = list.filter((tool) =>
|
|
9873
|
+
if (values.filter) list = list.filter((tool) => matchesFilter(values.filter, tool));
|
|
9604
9874
|
if (values.capability) list = list.filter((tool) => matchesCapability(values.capability, tool.capability));
|
|
9605
9875
|
if (values.json) io.stdout(JSON.stringify(list, null, 2));
|
|
9606
9876
|
else for (const line of formatToolList(list)) io.stdout(line);
|
|
9607
9877
|
return ExitCode.Ok;
|
|
9608
9878
|
}
|
|
9609
9879
|
case "capabilities": {
|
|
9610
|
-
const { values } =
|
|
9880
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
|
|
9611
9881
|
const { client: tools } = await client(values, io);
|
|
9612
9882
|
const list = await tools.listTools();
|
|
9613
9883
|
if (values.json) io.stdout(JSON.stringify(capabilityClusters(list), null, 2));
|
|
@@ -9615,103 +9885,75 @@ async function runTools(args, io) {
|
|
|
9615
9885
|
return ExitCode.Ok;
|
|
9616
9886
|
}
|
|
9617
9887
|
case "describe": {
|
|
9618
|
-
const { values, positionals } =
|
|
9888
|
+
const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
|
|
9619
9889
|
const name = positionals[0];
|
|
9620
9890
|
if (!name) throw new CliError("describe needs a tool name");
|
|
9621
9891
|
const { settings, client: tools } = await client(values, io);
|
|
9622
|
-
const
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9892
|
+
const described = await tools.describeTool(name);
|
|
9893
|
+
if ("notFound" in described) {
|
|
9894
|
+
const close = (await tools.listTools()).filter((entry) => entry.name.includes(name)).map((entry) => entry.name).slice(0, 5);
|
|
9895
|
+
throw new CliError(`${described.notFound}${close.length ? `
|
|
9896
|
+
Did you mean: ${close.join(", ")}?` : ""}`);
|
|
9627
9897
|
}
|
|
9898
|
+
const { tool } = described;
|
|
9628
9899
|
if (values.json) io.stdout(JSON.stringify(tool, null, 2));
|
|
9629
9900
|
else for (const line of formatDescribe(tool, settings.organizationId)) io.stdout(line);
|
|
9630
9901
|
return ExitCode.Ok;
|
|
9631
9902
|
}
|
|
9632
|
-
case "call":
|
|
9633
|
-
|
|
9634
|
-
const name = positionals[0];
|
|
9635
|
-
if (!name) throw new CliError("call needs a tool name");
|
|
9636
|
-
const parsedArgs = resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
|
|
9637
|
-
const timeoutMs = Number(values.timeout) * 1e3;
|
|
9638
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
|
|
9639
|
-
const { settings, client: tools } = await client(values, io);
|
|
9640
|
-
const body = await tools.callTool(name, parsedArgs);
|
|
9641
|
-
if (body.success === false) throw new CliError(body.error || "Tool execution failed");
|
|
9642
|
-
if (!isPending(body.result)) {
|
|
9643
|
-
io.stdout(JSON.stringify(body.result ?? null, null, 2));
|
|
9644
|
-
return ExitCode.Ok;
|
|
9645
|
-
}
|
|
9646
|
-
for (const line of formatPending(body.result, settings.apiUrl, settings.organizationId)) io.stdout(line);
|
|
9647
|
-
if (!values.wait) return ExitCode.Pending;
|
|
9648
|
-
const invocation = await waitForInvocation(body.result.invocationId ?? "", { pollAfterMs: body.result.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
|
|
9649
|
-
if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
|
|
9650
|
-
io.stderr(`Still ${invocation.status} after ${values.timeout}s; the invocation remains open.`);
|
|
9651
|
-
return ExitCode.Pending;
|
|
9652
|
-
}
|
|
9653
|
-
return emit(io, settledOutcome(invocation));
|
|
9654
|
-
}
|
|
9903
|
+
case "call":
|
|
9904
|
+
return call(rest, io);
|
|
9655
9905
|
case "run": {
|
|
9656
|
-
const { values, positionals } =
|
|
9906
|
+
const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
|
|
9657
9907
|
const file = positionals[0];
|
|
9658
|
-
if (!file) throw new CliError("run needs a file of calls");
|
|
9908
|
+
if (!file) throw new CliError("run needs a file of calls, or - for stdin");
|
|
9659
9909
|
let entries;
|
|
9660
9910
|
try {
|
|
9661
|
-
entries = parseBatch(fs10.readFileSync(file, "utf8"));
|
|
9911
|
+
entries = parseBatch(file === "-" ? await readStdin() : fs10.readFileSync(file, "utf8"));
|
|
9662
9912
|
} catch (error) {
|
|
9663
9913
|
throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9664
9914
|
}
|
|
9665
|
-
const timeoutMs =
|
|
9666
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
|
|
9915
|
+
const timeoutMs = timeoutOf(values.timeout);
|
|
9667
9916
|
const { settings, client: tools } = await client(values, io);
|
|
9668
9917
|
const { exitCode } = await runBatch(entries, {
|
|
9669
9918
|
callTool: (tool, args2) => tools.callTool(tool, args2),
|
|
9670
9919
|
decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
|
|
9671
9920
|
onOutcome: (outcome) => io.stdout(JSON.stringify(outcome)),
|
|
9672
|
-
...values.wait ? {
|
|
9673
|
-
waitFor: async (pending) => {
|
|
9674
|
-
const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
|
|
9675
|
-
return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
|
|
9676
|
-
}
|
|
9677
|
-
} : {}
|
|
9921
|
+
...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
|
|
9678
9922
|
});
|
|
9679
9923
|
return exitCode;
|
|
9680
9924
|
}
|
|
9681
9925
|
case "invocation": {
|
|
9682
|
-
const { values, positionals } =
|
|
9926
|
+
const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
|
|
9683
9927
|
const id = positionals[0];
|
|
9684
9928
|
if (!id) throw new CliError("invocation needs an id");
|
|
9685
9929
|
const { client: tools } = await client(values, io);
|
|
9686
9930
|
let invocation;
|
|
9687
9931
|
if (values.wait) invocation = await waitForInvocation(id, { timeoutMs: Number(values.timeout) * 1e3 }, { getInvocation: (target) => tools.getInvocation(target), sleep: realSleep, now: Date.now });
|
|
9688
9932
|
else invocation = (await tools.getInvocation(id)).invocation ?? {};
|
|
9933
|
+
const outcome = readBackOutcomeOf(invocation.toolName ?? "", id, invocation);
|
|
9689
9934
|
if (values.json) {
|
|
9690
9935
|
io.stdout(JSON.stringify(invocation, null, 2));
|
|
9691
|
-
return ExitCode.
|
|
9692
|
-
}
|
|
9693
|
-
if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
|
|
9694
|
-
io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
|
|
9695
|
-
return ExitCode.Pending;
|
|
9936
|
+
return outcome ? worstExitCode([outcome]) : ExitCode.Failure;
|
|
9696
9937
|
}
|
|
9697
|
-
|
|
9938
|
+
if (!outcome) throw new CliError(`Unexpected invocation status: ${invocation.status}`);
|
|
9939
|
+
if (outcome.status === "pending") io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
|
|
9940
|
+
else printOutcome(outcome, "", io);
|
|
9941
|
+
return worstExitCode([outcome]);
|
|
9698
9942
|
}
|
|
9699
9943
|
case "bridge": {
|
|
9700
|
-
const { values } =
|
|
9944
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
|
|
9701
9945
|
const { client: tools } = await client(values, io);
|
|
9702
9946
|
const allow = (values.tools ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
9703
9947
|
return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
|
|
9704
9948
|
}
|
|
9705
9949
|
default:
|
|
9706
|
-
throw
|
|
9707
|
-
|
|
9708
|
-
${toolsUsage}`);
|
|
9950
|
+
throw unknownVerb("tools", toolsUsage, [], verb, rest);
|
|
9709
9951
|
}
|
|
9710
9952
|
}
|
|
9711
9953
|
|
|
9712
9954
|
// src/commands/plugins.ts
|
|
9713
9955
|
import fs11 from "node:fs";
|
|
9714
|
-
import { parseArgs as
|
|
9956
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
9715
9957
|
var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
|
|
9716
9958
|
|
|
9717
9959
|
validate <manifest.json> (dry run: the platform's full validation, nothing stored)
|
|
@@ -9755,26 +9997,15 @@ function formatPlugin(plugin) {
|
|
|
9755
9997
|
return [
|
|
9756
9998
|
`${plugin.pluginId}@${plugin.version} ${plugin.name} (${parent}; runtime ${plugin.runtimeType}; digest ${plugin.digest.slice(0, 12)})`,
|
|
9757
9999
|
` tools: ${plugin.toolNames.length ? plugin.toolNames.join(", ") : "none"}`,
|
|
9758
|
-
...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : []
|
|
10000
|
+
...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : [],
|
|
10001
|
+
...plugin.refusals?.length ? [" NOT SERVED: the platform refuses this version; publish a fixed one:", ...plugin.refusals.map((reason) => ` - ${reason}`)] : []
|
|
9759
10002
|
];
|
|
9760
10003
|
}
|
|
9761
|
-
function pluginsVerbUsage(verb) {
|
|
9762
|
-
const lines = pluginsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `));
|
|
9763
|
-
return lines.length ? `diverge plugins ${verb} ...
|
|
9764
|
-
|
|
9765
|
-
${lines.join("\n")}
|
|
9766
|
-
|
|
9767
|
-
Every plugins command takes --organization-id <id> (or DIVERGENT_ORG_ID, else the signed-in login's organization).` : pluginsUsage;
|
|
9768
|
-
}
|
|
9769
10004
|
async function runPlugins(args, io) {
|
|
9770
10005
|
const [verb, ...rest] = args;
|
|
9771
|
-
if (verb && (rest.includes("--help") || rest.includes("-h"))) {
|
|
9772
|
-
io.stdout(pluginsVerbUsage(verb));
|
|
9773
|
-
return ExitCode.Ok;
|
|
9774
|
-
}
|
|
9775
10006
|
switch (verb) {
|
|
9776
10007
|
case "validate": {
|
|
9777
|
-
const { values, positionals } =
|
|
10008
|
+
const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
|
|
9778
10009
|
const manifest = readManifest(positionals[0]);
|
|
9779
10010
|
const tools = await client2(values, io);
|
|
9780
10011
|
const outcome = await tools.publishPlugin(manifest, { dryRun: true });
|
|
@@ -9791,7 +10022,7 @@ async function runPlugins(args, io) {
|
|
|
9791
10022
|
return ExitCode.Ok;
|
|
9792
10023
|
}
|
|
9793
10024
|
case "publish": {
|
|
9794
|
-
const { values, positionals } =
|
|
10025
|
+
const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
|
|
9795
10026
|
const manifest = readManifest(positionals[0]);
|
|
9796
10027
|
const tools = await client2(values, io);
|
|
9797
10028
|
const outcome = await tools.publishPlugin(manifest, { dryRun: false });
|
|
@@ -9809,7 +10040,7 @@ async function runPlugins(args, io) {
|
|
|
9809
10040
|
return ExitCode.Ok;
|
|
9810
10041
|
}
|
|
9811
10042
|
case "list": {
|
|
9812
|
-
const { values } =
|
|
10043
|
+
const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
|
|
9813
10044
|
const tools = await client2(values, io);
|
|
9814
10045
|
const plugins = await tools.listPlugins();
|
|
9815
10046
|
if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
|
|
@@ -9818,7 +10049,7 @@ async function runPlugins(args, io) {
|
|
|
9818
10049
|
return ExitCode.Ok;
|
|
9819
10050
|
}
|
|
9820
10051
|
case "retire": {
|
|
9821
|
-
const { values, positionals } =
|
|
10052
|
+
const { values, positionals } = parseArgs7({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
|
|
9822
10053
|
const pluginId = positionals[0];
|
|
9823
10054
|
if (!pluginId) throw new CliError(`A plugin id is required.
|
|
9824
10055
|
|
|
@@ -9829,9 +10060,7 @@ ${pluginsUsage}`);
|
|
|
9829
10060
|
return ExitCode.Ok;
|
|
9830
10061
|
}
|
|
9831
10062
|
default:
|
|
9832
|
-
throw
|
|
9833
|
-
|
|
9834
|
-
${pluginsUsage}`);
|
|
10063
|
+
throw unknownVerb("plugins", pluginsUsage, [], verb, rest);
|
|
9835
10064
|
}
|
|
9836
10065
|
}
|
|
9837
10066
|
|
|
@@ -9841,11 +10070,11 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
9841
10070
|
// src/update-notice.ts
|
|
9842
10071
|
import { spawn as spawn3 } from "node:child_process";
|
|
9843
10072
|
import fs12 from "node:fs";
|
|
9844
|
-
import
|
|
10073
|
+
import path14 from "node:path";
|
|
9845
10074
|
var UPDATE_CHECK_FILE = "update-check.json";
|
|
9846
10075
|
var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9847
10076
|
function updateCheckFile(env) {
|
|
9848
|
-
return
|
|
10077
|
+
return path14.join(configDir(env), UPDATE_CHECK_FILE);
|
|
9849
10078
|
}
|
|
9850
10079
|
function readUpdateCheck(env) {
|
|
9851
10080
|
try {
|
|
@@ -9942,9 +10171,9 @@ function installLatest(version) {
|
|
|
9942
10171
|
|
|
9943
10172
|
// src/cli.ts
|
|
9944
10173
|
var COMMANDS = [
|
|
9945
|
-
{ name: "login", summary: "Sign in through the browser", usage: loginUsage, run: runLogin },
|
|
10174
|
+
{ name: "login", summary: "Sign in (authenticate) through the browser", usage: loginUsage, run: runLogin },
|
|
9946
10175
|
{ name: "logout", summary: "Revoke the login and forget it", usage: logoutUsage, run: runLogout },
|
|
9947
|
-
{ name: "status", summary: "Show the deployment and the
|
|
10176
|
+
{ name: "status", summary: "Show who you are signed in as: the deployment, the account and what the login carries", usage: statusUsage, run: runStatus },
|
|
9948
10177
|
{ name: "org", summary: "Show or switch the organization the login acts in", usage: orgUsage, run: runOrg },
|
|
9949
10178
|
{ name: "token", summary: "Print a fresh access token", usage: tokenUsage, run: runToken },
|
|
9950
10179
|
{ name: "agents", summary: "Point a coding agent at the model gateway (setup, run, status)", usage: agentsUsage, run: runAgents },
|
|
@@ -9965,7 +10194,7 @@ function usage() {
|
|
|
9965
10194
|
...lines,
|
|
9966
10195
|
"",
|
|
9967
10196
|
"Run `diverge <command> --help` for its options.",
|
|
9968
|
-
"Environment: DIVERGENT_API_URL (deployment), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (
|
|
10197
|
+
"Environment: DIVERGENT_API_URL (deployment; production when unset), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (the organization and project a command acts in)."
|
|
9969
10198
|
].join("\n");
|
|
9970
10199
|
}
|
|
9971
10200
|
async function run2(argv, io) {
|
|
@@ -9980,15 +10209,19 @@ async function run2(argv, io) {
|
|
|
9980
10209
|
}
|
|
9981
10210
|
const command = COMMANDS.find((candidate) => candidate.name === name);
|
|
9982
10211
|
if (!command) {
|
|
9983
|
-
|
|
10212
|
+
const candidates = COMMANDS.map((known) => ({ invocation: `diverge ${known.name}`, name: known.name, summary: known.summary, lines: [known.summary] }));
|
|
10213
|
+
io.stderr(`Unknown command: ${name}.${didYouMean(closest(name, candidates, rest))}
|
|
9984
10214
|
|
|
9985
10215
|
${usage()}`);
|
|
9986
10216
|
return ExitCode.Failure;
|
|
9987
10217
|
}
|
|
9988
10218
|
const own = rest.includes("--") ? rest.slice(0, rest.indexOf("--")) : rest;
|
|
9989
|
-
if (
|
|
9990
|
-
|
|
9991
|
-
|
|
10219
|
+
if (own.includes("--help") || own.includes("-h")) {
|
|
10220
|
+
const path15 = verbPath(command.usage, own);
|
|
10221
|
+
if (path15.length > 0 || own[0].startsWith("-")) {
|
|
10222
|
+
io.stdout(verbUsage(command.name, command.usage, path15));
|
|
10223
|
+
return ExitCode.Ok;
|
|
10224
|
+
}
|
|
9992
10225
|
}
|
|
9993
10226
|
const code2 = await runCommand(command, rest, io);
|
|
9994
10227
|
updateNotice(argv, io);
|
|
@@ -10003,9 +10236,9 @@ async function runCommand(command, rest, io) {
|
|
|
10003
10236
|
return error.exitCode;
|
|
10004
10237
|
}
|
|
10005
10238
|
if (error instanceof Error && error.name === "TypeError" && /Unknown option|Option .* argument|Unexpected argument/.test(error.message)) {
|
|
10006
|
-
io.stderr(`Error: ${error.message}
|
|
10239
|
+
io.stderr(`Error: ${unknownOption(error, command.usage, rest) ?? error.message}
|
|
10007
10240
|
|
|
10008
|
-
${command.usage}`);
|
|
10241
|
+
${verbUsage(command.name, command.usage, verbPath(command.usage, rest))}`);
|
|
10009
10242
|
return ExitCode.Failure;
|
|
10010
10243
|
}
|
|
10011
10244
|
io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}`);
|