@dashai/cli 0.26.0 → 0.27.0
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/dist/bin.js +496 -35
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, read
|
|
|
4
4
|
import { hostname, tmpdir, platform, homedir } from 'os';
|
|
5
5
|
import { resolve, join, relative, dirname, basename, win32, sep } from 'path';
|
|
6
6
|
import { execFileSync, spawn, spawnSync } from 'child_process';
|
|
7
|
-
import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm, multiselect } from '@clack/prompts';
|
|
7
|
+
import { intro, log, select, isCancel, cancel, spinner, outro, text, confirm as confirm$1, multiselect } from '@clack/prompts';
|
|
8
8
|
import { Command, Option } from 'commander';
|
|
9
9
|
import open from 'open';
|
|
10
10
|
import AdmZip from 'adm-zip';
|
|
@@ -1187,16 +1187,16 @@ async function devCommand(options) {
|
|
|
1187
1187
|
}
|
|
1188
1188
|
const callbackOrigins = computeCallbackOrigins(port, options.callbackOrigin ?? []);
|
|
1189
1189
|
try {
|
|
1190
|
-
const
|
|
1190
|
+
const session2 = await registerDevSession(apiUrl, apiToken, {
|
|
1191
1191
|
target_workspace_id: workspaceContext.id,
|
|
1192
1192
|
callback_origins: callbackOrigins
|
|
1193
1193
|
});
|
|
1194
|
-
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken,
|
|
1195
|
-
devHandoffEnv = devSessionHandoffEnv(
|
|
1196
|
-
info(` ${dim("dev session")} ${
|
|
1194
|
+
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session2.id);
|
|
1195
|
+
devHandoffEnv = devSessionHandoffEnv(session2);
|
|
1196
|
+
info(` ${dim("dev session")} ${session2.id}`);
|
|
1197
1197
|
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
1198
1198
|
info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
|
|
1199
|
-
info(` ${dim("expires")} ${
|
|
1199
|
+
info(` ${dim("expires")} ${session2.expires_at}`);
|
|
1200
1200
|
info(
|
|
1201
1201
|
` ${dim("sign-in")} ${Object.keys(devHandoffEnv).length > 0 ? "zero-click (server-side dev handoff armed)" : "browser SSO (backend issued no handoff secret \u2014 pre-2026-07-15 backend)"}`
|
|
1202
1202
|
);
|
|
@@ -1455,11 +1455,11 @@ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
|
1455
1455
|
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
1456
1456
|
return upgraded;
|
|
1457
1457
|
}
|
|
1458
|
-
function devSessionHandoffEnv(
|
|
1459
|
-
const secret =
|
|
1458
|
+
function devSessionHandoffEnv(session2) {
|
|
1459
|
+
const secret = session2.handoff_secret;
|
|
1460
1460
|
if (typeof secret !== "string" || secret.length === 0) return {};
|
|
1461
1461
|
return {
|
|
1462
|
-
DASHWISE_DEV_SESSION_ID:
|
|
1462
|
+
DASHWISE_DEV_SESSION_ID: session2.id,
|
|
1463
1463
|
DASHWISE_DEV_HANDOFF_SECRET: secret
|
|
1464
1464
|
};
|
|
1465
1465
|
}
|
|
@@ -12853,9 +12853,7 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
|
|
|
12853
12853
|
person in plain words that the changes are saved and that Release in
|
|
12854
12854
|
Dashwise includes them. Don't ask them to merge anything.
|
|
12855
12855
|
- Never force-push and never rewrite the default branch's history.
|
|
12856
|
-
-
|
|
12857
|
-
pushed, tell them it is ready: in Dashwise open the module, click Release,
|
|
12858
|
-
then Take live.
|
|
12856
|
+
- When the work is pushed, tell the person it is ready to release.
|
|
12859
12857
|
|
|
12860
12858
|
## Cloud workflow
|
|
12861
12859
|
|
|
@@ -12866,8 +12864,8 @@ or in their own \`CLAUDE.md\` / \`AGENTS.md\`), follow them instead.
|
|
|
12866
12864
|
- \`npm run smoke:production\` starts the built app on a local loopback port. If a cloud sandbox refuses that port, rerun the command with the network permission; never skip the check.
|
|
12867
12865
|
- Report what you changed, the pushed SHA and the checks you ran, including failures, in plain words.
|
|
12868
12866
|
- Never print, persist or commit secrets. Repository code delivery needs no Dashwise token. Do not log in or access company data for setup or checks; see "Dashwise login" for full development.
|
|
12869
|
-
-
|
|
12870
|
-
-
|
|
12867
|
+
- Before releasing, installing/updating an installation or taking live, propose the exact action in the chat and wait for the person to accept or change it; never do it on your own.
|
|
12868
|
+
- When the person accepts, do exactly that action if your Dashwise connection allows it; if Dashwise refuses, tell them what it said (for example a permission to turn on in My Account). If you have no way to do it, tell them in one sentence where to click (in Dashwise open the module, click Release, then Take live). Never ask for a \`dashwise login\` or a token just to release.
|
|
12871
12869
|
|
|
12872
12870
|
## If you can't see the repository
|
|
12873
12871
|
|
|
@@ -14394,7 +14392,7 @@ async function destroyDevInstallCommand(options) {
|
|
|
14394
14392
|
return 0;
|
|
14395
14393
|
}
|
|
14396
14394
|
if (!options.yes && process.stdout.isTTY) {
|
|
14397
|
-
const choice = await confirm({
|
|
14395
|
+
const choice = await confirm$1({
|
|
14398
14396
|
message: `Destroy dev install ${code(`${slug} (id=${entry.installationId})`)}? Tables and rows will be soft-deleted (recoverable for 30 days via dashboard trash).`
|
|
14399
14397
|
});
|
|
14400
14398
|
if (isCancel(choice) || !choice) {
|
|
@@ -16335,7 +16333,7 @@ function startMcpProxy(opts) {
|
|
|
16335
16333
|
const reconnectMs = opts.reconnectMs ?? 2e3;
|
|
16336
16334
|
const verbose = opts.verbose === true;
|
|
16337
16335
|
const readOnly = opts.readOnly === true;
|
|
16338
|
-
let
|
|
16336
|
+
let session2 = opts.session;
|
|
16339
16337
|
let sessionId = null;
|
|
16340
16338
|
let protocolVersion = null;
|
|
16341
16339
|
let readTools = null;
|
|
@@ -16364,7 +16362,7 @@ function startMcpProxy(opts) {
|
|
|
16364
16362
|
parsed = JSON.parse(text4);
|
|
16365
16363
|
} catch {
|
|
16366
16364
|
log2(
|
|
16367
|
-
`Discarded a non-JSON response from ${redactUrl(
|
|
16365
|
+
`Discarded a non-JSON response from ${redactUrl(session2.connectUrl)} (${source}); stdout carries MCP frames only.`
|
|
16368
16366
|
);
|
|
16369
16367
|
trace(`discarded ${Buffer.byteLength(text4, "utf8")} bytes of non-JSON`);
|
|
16370
16368
|
return;
|
|
@@ -16374,7 +16372,7 @@ function startMcpProxy(opts) {
|
|
|
16374
16372
|
) : parsed !== null && typeof parsed === "object" && "jsonrpc" in parsed;
|
|
16375
16373
|
if (!looksJsonRpc) {
|
|
16376
16374
|
log2(
|
|
16377
|
-
`Discarded a JSON response from ${redactUrl(
|
|
16375
|
+
`Discarded a JSON response from ${redactUrl(session2.connectUrl)} that is not JSON-RPC (${source}).`
|
|
16378
16376
|
);
|
|
16379
16377
|
return;
|
|
16380
16378
|
}
|
|
@@ -16405,7 +16403,7 @@ function startMcpProxy(opts) {
|
|
|
16405
16403
|
reauthAttempt = (async () => {
|
|
16406
16404
|
log2("The session key expired or was rotated \u2014 re-authenticating once.");
|
|
16407
16405
|
try {
|
|
16408
|
-
|
|
16406
|
+
session2 = await reauth();
|
|
16409
16407
|
sessionId = null;
|
|
16410
16408
|
return true;
|
|
16411
16409
|
} catch (err) {
|
|
@@ -16446,7 +16444,7 @@ function startMcpProxy(opts) {
|
|
|
16446
16444
|
};
|
|
16447
16445
|
const post = async (text4, isInitialize) => {
|
|
16448
16446
|
if (stopped) return null;
|
|
16449
|
-
return doFetch(
|
|
16447
|
+
return doFetch(session2.connectUrl, {
|
|
16450
16448
|
method: "POST",
|
|
16451
16449
|
headers: requestHeaders(isInitialize),
|
|
16452
16450
|
body: text4
|
|
@@ -16474,7 +16472,7 @@ function startMcpProxy(opts) {
|
|
|
16474
16472
|
res = await post(text4, isInitialize);
|
|
16475
16473
|
} catch (err) {
|
|
16476
16474
|
const message = err.message;
|
|
16477
|
-
log2(`Transport error talking to ${redactUrl(
|
|
16475
|
+
log2(`Transport error talking to ${redactUrl(session2.connectUrl)}: ${message}`);
|
|
16478
16476
|
emit(transportErrorFrame(frame?.id ?? null, `Transport error: ${message}`));
|
|
16479
16477
|
return { kind: "failed" };
|
|
16480
16478
|
}
|
|
@@ -16498,7 +16496,7 @@ function startMcpProxy(opts) {
|
|
|
16498
16496
|
const bodyText = await res.text();
|
|
16499
16497
|
if (!res.ok) {
|
|
16500
16498
|
const detail = res.status === 401 ? "the endpoint key is no longer valid (expired, rotated by another `dashwise mcp`, or revoked)" : `HTTP ${res.status}`;
|
|
16501
|
-
log2(`Refused by ${redactUrl(
|
|
16499
|
+
log2(`Refused by ${redactUrl(session2.connectUrl)}: ${detail}`);
|
|
16502
16500
|
emit(transportErrorFrame(frame?.id ?? null, `Dashwise MCP server: ${detail}`));
|
|
16503
16501
|
if (res.status === 401) {
|
|
16504
16502
|
exitCode = 1;
|
|
@@ -16585,7 +16583,7 @@ function startMcpProxy(opts) {
|
|
|
16585
16583
|
streamAbort = new AbortController();
|
|
16586
16584
|
let res;
|
|
16587
16585
|
try {
|
|
16588
|
-
res = await doFetch(
|
|
16586
|
+
res = await doFetch(session2.connectUrl, {
|
|
16589
16587
|
method: "GET",
|
|
16590
16588
|
headers: {
|
|
16591
16589
|
Accept: "text/event-stream",
|
|
@@ -16621,7 +16619,7 @@ function startMcpProxy(opts) {
|
|
|
16621
16619
|
}
|
|
16622
16620
|
streamUnavailable = true;
|
|
16623
16621
|
log2(
|
|
16624
|
-
`Server stream refused by ${redactUrl(
|
|
16622
|
+
`Server stream refused by ${redactUrl(session2.connectUrl)}: the session could not be renewed (the endpoint key expired, was rotated, or was revoked).`
|
|
16625
16623
|
);
|
|
16626
16624
|
emit(
|
|
16627
16625
|
transportErrorFrame(
|
|
@@ -16898,9 +16896,9 @@ async function mcpCommand(options = {}) {
|
|
|
16898
16896
|
return 1;
|
|
16899
16897
|
}
|
|
16900
16898
|
const label = options.endpointName ?? defaultEndpointLabel();
|
|
16901
|
-
let
|
|
16899
|
+
let session2;
|
|
16902
16900
|
try {
|
|
16903
|
-
|
|
16901
|
+
session2 = await openCliSession({
|
|
16904
16902
|
workspaceId: workspace2.id,
|
|
16905
16903
|
label,
|
|
16906
16904
|
...scopes !== void 0 ? { scopes } : {}
|
|
@@ -16909,24 +16907,24 @@ async function mcpCommand(options = {}) {
|
|
|
16909
16907
|
fail(describeSessionError(err, apiUrl, scopes));
|
|
16910
16908
|
return 1;
|
|
16911
16909
|
}
|
|
16912
|
-
if (scopes !== void 0 && !Array.isArray(
|
|
16913
|
-
await revokeSession(
|
|
16910
|
+
if (scopes !== void 0 && !Array.isArray(session2.scopes)) {
|
|
16911
|
+
await revokeSession(session2.endpointId);
|
|
16914
16912
|
fail(SCOPES_NOT_APPLIED);
|
|
16915
16913
|
return 1;
|
|
16916
16914
|
}
|
|
16917
16915
|
info(
|
|
16918
|
-
`Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${
|
|
16916
|
+
`Dashwise MCP \u2014 workspace ${workspace2.slug}, endpoint #${session2.endpointId} (${session2.name}).`
|
|
16919
16917
|
);
|
|
16920
|
-
info(`Session key expires ${dim(
|
|
16921
|
-
info(describeScopes(
|
|
16918
|
+
info(`Session key expires ${dim(session2.expiresAt)}.`);
|
|
16919
|
+
info(describeScopes(session2.scopes));
|
|
16922
16920
|
if (options.readOnly) {
|
|
16923
16921
|
warn(
|
|
16924
16922
|
"Read-only: write tools are refused by this proxy (a client-side guard, not a server-enforced scope \u2014 for that, use --scope)."
|
|
16925
16923
|
);
|
|
16926
16924
|
}
|
|
16927
|
-
let ownedEndpointId =
|
|
16925
|
+
let ownedEndpointId = session2.endpointId;
|
|
16928
16926
|
const proxy = startMcpProxy({
|
|
16929
|
-
session,
|
|
16927
|
+
session: session2,
|
|
16930
16928
|
reauth: async () => {
|
|
16931
16929
|
const fresh = await openCliSession({
|
|
16932
16930
|
workspaceId: workspace2.id,
|
|
@@ -20249,6 +20247,22 @@ function refusalHint(code2, context, subject = {}) {
|
|
|
20249
20247
|
message: `Too many builds of ${named} in the last hour. Try again later.`,
|
|
20250
20248
|
needsGrant: false
|
|
20251
20249
|
};
|
|
20250
|
+
// Install invitations (`dashwise module invites`, Südbau retro 6).
|
|
20251
|
+
case "AUTHOR_ADMIN_REQUIRED":
|
|
20252
|
+
return {
|
|
20253
|
+
message: `Only an admin of the company that owns ${named} can manage its install invitations. Ask that company's admin.`,
|
|
20254
|
+
needsGrant: true
|
|
20255
|
+
};
|
|
20256
|
+
case "COMPANY_ADMIN_REQUIRED":
|
|
20257
|
+
return {
|
|
20258
|
+
message: "Only an admin of this company can see or decline its install invitations. Ask your company's admin.",
|
|
20259
|
+
needsGrant: true
|
|
20260
|
+
};
|
|
20261
|
+
case "MODULE_PRIVATE_SHARE_FIRST":
|
|
20262
|
+
return {
|
|
20263
|
+
message: `${named} is still private. Share it with your company in Dashwise first, then invite another company.`,
|
|
20264
|
+
needsGrant: false
|
|
20265
|
+
};
|
|
20252
20266
|
default:
|
|
20253
20267
|
if (MODULE_RIGHT_CODES.has(code2)) {
|
|
20254
20268
|
return {
|
|
@@ -20658,6 +20672,133 @@ function usesJson(readiness) {
|
|
|
20658
20672
|
};
|
|
20659
20673
|
}
|
|
20660
20674
|
|
|
20675
|
+
// src/commands/module/installations.ts
|
|
20676
|
+
init_api_client();
|
|
20677
|
+
init_config();
|
|
20678
|
+
init_output();
|
|
20679
|
+
var AUDIENCE = {
|
|
20680
|
+
company: "company",
|
|
20681
|
+
teams: "Team Spaces",
|
|
20682
|
+
people: "named people"
|
|
20683
|
+
};
|
|
20684
|
+
var givesRole = (roleKey) => typeof roleKey === "string" && roleKey !== "none";
|
|
20685
|
+
function governanceAudience(row) {
|
|
20686
|
+
if (!row.rules) return null;
|
|
20687
|
+
if (givesRole(row.rules.company)) return AUDIENCE.company;
|
|
20688
|
+
if (row.rules.teams.some((t) => t.live === true && givesRole(t.role_key))) return AUDIENCE.teams;
|
|
20689
|
+
return AUDIENCE.people;
|
|
20690
|
+
}
|
|
20691
|
+
function toListEntry(row) {
|
|
20692
|
+
return {
|
|
20693
|
+
id: row.id,
|
|
20694
|
+
module_slug: row.module?.slug ?? null,
|
|
20695
|
+
name: row.name ?? row.module?.name ?? null,
|
|
20696
|
+
audience: row.reach?.kind ? AUDIENCE[row.reach.kind] ?? row.reach.kind : null,
|
|
20697
|
+
version: row.module_version?.version ?? null,
|
|
20698
|
+
held: row.held === true,
|
|
20699
|
+
development: isDevInstallKind(row.kind),
|
|
20700
|
+
public_url: row.public_url,
|
|
20701
|
+
admin_view_only: false
|
|
20702
|
+
};
|
|
20703
|
+
}
|
|
20704
|
+
async function moduleInstallationsCommand(slug, options) {
|
|
20705
|
+
if (!isLoggedIn()) {
|
|
20706
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
20707
|
+
return 1;
|
|
20708
|
+
}
|
|
20709
|
+
const cfg = readConfig();
|
|
20710
|
+
if (!cfg) {
|
|
20711
|
+
fail(`CLI config missing. Run ${code("dashwise login")}.`);
|
|
20712
|
+
return 1;
|
|
20713
|
+
}
|
|
20714
|
+
const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
|
|
20715
|
+
let workspace2;
|
|
20716
|
+
try {
|
|
20717
|
+
workspace2 = await resolveWorkspaceWithFallback({
|
|
20718
|
+
userFlag: options.workspace,
|
|
20719
|
+
cachedWorkspaceId: slug ? readModuleEntry(slug)?.workspaceId : void 0,
|
|
20720
|
+
configDefault: cfg.workspaceSlug,
|
|
20721
|
+
apiUrl: resolveApiUrl(),
|
|
20722
|
+
apiToken: cfg.token
|
|
20723
|
+
});
|
|
20724
|
+
} catch (err) {
|
|
20725
|
+
fail(`Company selection failed: ${err.message}`);
|
|
20726
|
+
return 1;
|
|
20727
|
+
}
|
|
20728
|
+
let rows;
|
|
20729
|
+
try {
|
|
20730
|
+
rows = await listWorkspaceInstallations(workspace2.id, auth);
|
|
20731
|
+
} catch (err) {
|
|
20732
|
+
fail(`Could not read the installations in ${code(workspace2.slug)}: ${err.message}`);
|
|
20733
|
+
return 1;
|
|
20734
|
+
}
|
|
20735
|
+
const governance = [];
|
|
20736
|
+
try {
|
|
20737
|
+
const govRows = await apiRequest(
|
|
20738
|
+
"GET",
|
|
20739
|
+
`/api/modules/workspaces/${workspace2.id}/installations/governance`,
|
|
20740
|
+
void 0,
|
|
20741
|
+
auth
|
|
20742
|
+
);
|
|
20743
|
+
const seen = new Set(rows.map((row) => row.id));
|
|
20744
|
+
const slugOf = new Map(rows.map((row) => [row.module_id, row.module?.slug ?? null]));
|
|
20745
|
+
let slugModuleId;
|
|
20746
|
+
if (slug) {
|
|
20747
|
+
slugModuleId = rows.find((row) => row.module?.slug === slug)?.module_id;
|
|
20748
|
+
if (slugModuleId === void 0) {
|
|
20749
|
+
try {
|
|
20750
|
+
slugModuleId = (await getModuleBySlug(slug, auth)).id;
|
|
20751
|
+
} catch {
|
|
20752
|
+
}
|
|
20753
|
+
}
|
|
20754
|
+
}
|
|
20755
|
+
for (const g of govRows) {
|
|
20756
|
+
if (g.trashed || seen.has(g.installation_id)) continue;
|
|
20757
|
+
if (slug && g.module_id !== slugModuleId) continue;
|
|
20758
|
+
governance.push({
|
|
20759
|
+
id: g.installation_id,
|
|
20760
|
+
module_slug: slugOf.get(g.module_id) ?? (slug && g.module_id === slugModuleId ? slug : null),
|
|
20761
|
+
name: g.name || null,
|
|
20762
|
+
audience: governanceAudience(g),
|
|
20763
|
+
version: null,
|
|
20764
|
+
held: g.held === true,
|
|
20765
|
+
development: null,
|
|
20766
|
+
public_url: null,
|
|
20767
|
+
admin_view_only: true
|
|
20768
|
+
});
|
|
20769
|
+
}
|
|
20770
|
+
} catch (err) {
|
|
20771
|
+
if (!(err instanceof ApiError && (err.status === 403 || err.status === 404))) {
|
|
20772
|
+
fail(`Could not read the company's full installation list: ${err.message}`);
|
|
20773
|
+
return 1;
|
|
20774
|
+
}
|
|
20775
|
+
}
|
|
20776
|
+
const entries = rows.filter((row) => !slug || row.module?.slug === slug).map(toListEntry).concat(governance).filter((entry) => options.dev || !entry.development).sort((a, b) => (a.module_slug ?? "").localeCompare(b.module_slug ?? "") || a.id - b.id);
|
|
20777
|
+
if (options.json) {
|
|
20778
|
+
out(JSON.stringify({ workspace: workspace2.slug, installations: entries }));
|
|
20779
|
+
return 0;
|
|
20780
|
+
}
|
|
20781
|
+
if (entries.length === 0) {
|
|
20782
|
+
info(
|
|
20783
|
+
slug ? `No installations of ${code(slug)} you can see in ${code(workspace2.slug)}.` : `No installations you can see in ${code(workspace2.slug)}.`
|
|
20784
|
+
);
|
|
20785
|
+
return 0;
|
|
20786
|
+
}
|
|
20787
|
+
for (const e of entries) {
|
|
20788
|
+
const tags = [
|
|
20789
|
+
e.audience,
|
|
20790
|
+
e.version ? `v${e.version}` : null,
|
|
20791
|
+
e.held ? "held" : null,
|
|
20792
|
+
e.development ? "development" : null,
|
|
20793
|
+
e.admin_view_only ? "admin view, not open to you" : null
|
|
20794
|
+
].filter(Boolean);
|
|
20795
|
+
out(
|
|
20796
|
+
`#${e.id} ${e.module_slug ?? "?"} ${e.name ? `"${e.name}"` : ""} ${dim(tags.join(", "))}${e.public_url ? ` ${e.public_url}` : ""}`.replace(/ {3,}/g, " ")
|
|
20797
|
+
);
|
|
20798
|
+
}
|
|
20799
|
+
return 0;
|
|
20800
|
+
}
|
|
20801
|
+
|
|
20661
20802
|
// src/commands/module/uninstall.ts
|
|
20662
20803
|
init_api_client();
|
|
20663
20804
|
init_config();
|
|
@@ -20828,6 +20969,269 @@ function surfaceUninstallError(err, slug, workspaceSlug) {
|
|
|
20828
20969
|
return 1;
|
|
20829
20970
|
}
|
|
20830
20971
|
|
|
20972
|
+
// src/commands/module/invites.ts
|
|
20973
|
+
init_api_client();
|
|
20974
|
+
init_config();
|
|
20975
|
+
init_output();
|
|
20976
|
+
var positiveId2 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
20977
|
+
function session(json) {
|
|
20978
|
+
const cfg = readConfig();
|
|
20979
|
+
if (!isLoggedIn() || !cfg) return reportCliError(`Not logged in. Run ${code("dashwise login")} first.`, { json });
|
|
20980
|
+
return {
|
|
20981
|
+
auth: { baseUrl: resolveApiUrl(), auth: { token: cfg.token } },
|
|
20982
|
+
token: cfg.token,
|
|
20983
|
+
workspaceSlug: cfg.workspaceSlug
|
|
20984
|
+
};
|
|
20985
|
+
}
|
|
20986
|
+
async function resolveModule(slug, s, json) {
|
|
20987
|
+
try {
|
|
20988
|
+
const mod = await getModuleBySlug(slug, s.auth);
|
|
20989
|
+
return { id: mod.id, slug: mod.slug };
|
|
20990
|
+
} catch (err) {
|
|
20991
|
+
return reportRefusal(err, { module: slug }, { json, action: "Resolving the module" });
|
|
20992
|
+
}
|
|
20993
|
+
}
|
|
20994
|
+
async function confirm(question, confirmLabel, opts) {
|
|
20995
|
+
if (opts.yes) return null;
|
|
20996
|
+
if (opts.json || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
20997
|
+
return reportCliError("This changes an invitation. Pass --yes to confirm in non-interactive or --json mode.", {
|
|
20998
|
+
json: opts.json,
|
|
20999
|
+
code: "USAGE"
|
|
21000
|
+
});
|
|
21001
|
+
}
|
|
21002
|
+
const answer = await select({
|
|
21003
|
+
message: question,
|
|
21004
|
+
options: [
|
|
21005
|
+
{ value: "cancel", label: "Cancel" },
|
|
21006
|
+
{ value: "go", label: confirmLabel }
|
|
21007
|
+
],
|
|
21008
|
+
initialValue: "cancel"
|
|
21009
|
+
});
|
|
21010
|
+
if (isCancel(answer) || answer !== "go") {
|
|
21011
|
+
reportCliError("Aborted.", {});
|
|
21012
|
+
return 130;
|
|
21013
|
+
}
|
|
21014
|
+
return null;
|
|
21015
|
+
}
|
|
21016
|
+
function companyLabel(row) {
|
|
21017
|
+
return row.company_name ? `${row.company_name} (company #${row.workspace_id})` : `company #${row.workspace_id}`;
|
|
21018
|
+
}
|
|
21019
|
+
async function moduleInvitesListCommand(slug, options) {
|
|
21020
|
+
const { json } = options;
|
|
21021
|
+
const s = session(json);
|
|
21022
|
+
if (typeof s === "number") return s;
|
|
21023
|
+
const mod = await resolveModule(slug, s, json);
|
|
21024
|
+
if (typeof mod === "number") return mod;
|
|
21025
|
+
let rows;
|
|
21026
|
+
try {
|
|
21027
|
+
rows = await apiRequest("GET", `/api/modules/${mod.id}/invites`, void 0, s.auth);
|
|
21028
|
+
} catch (err) {
|
|
21029
|
+
return reportRefusal(err, { module: mod.slug }, { json, action: "Reading the invitations" });
|
|
21030
|
+
}
|
|
21031
|
+
if (json) {
|
|
21032
|
+
out(JSON.stringify({ module: mod.slug, invites: rows }));
|
|
21033
|
+
return 0;
|
|
21034
|
+
}
|
|
21035
|
+
if (rows.length === 0) {
|
|
21036
|
+
info(`No open invitations for ${code(mod.slug)}.`);
|
|
21037
|
+
return 0;
|
|
21038
|
+
}
|
|
21039
|
+
for (const row of rows) {
|
|
21040
|
+
const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
|
|
21041
|
+
out(`#${row.id} ${companyLabel(row)} ${dim(row.invited_at.slice(0, 10))}${note}`);
|
|
21042
|
+
}
|
|
21043
|
+
return 0;
|
|
21044
|
+
}
|
|
21045
|
+
async function moduleInvitesSendCommand(slug, options) {
|
|
21046
|
+
const { json } = options;
|
|
21047
|
+
const usage2 = (message) => reportCliError(message, { json, code: "USAGE" });
|
|
21048
|
+
if (!options.to) return usage2("Pass --to <company id> to name the company to invite.");
|
|
21049
|
+
const s = session(json);
|
|
21050
|
+
if (typeof s === "number") return s;
|
|
21051
|
+
let target;
|
|
21052
|
+
const numeric = /^[1-9][0-9]*$/.test(options.to) ? Number(options.to) : void 0;
|
|
21053
|
+
let mine;
|
|
21054
|
+
try {
|
|
21055
|
+
mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
|
|
21056
|
+
} catch (err) {
|
|
21057
|
+
return reportRefusal(err, {}, { json, action: "Reading your companies" });
|
|
21058
|
+
}
|
|
21059
|
+
const own = mine.find((w) => numeric !== void 0 ? w.id === numeric : w.slug === options.to);
|
|
21060
|
+
if (own) {
|
|
21061
|
+
target = { id: own.id, label: `${own.name} (company #${own.id})` };
|
|
21062
|
+
} else if (positiveId2(numeric)) {
|
|
21063
|
+
target = { id: numeric, label: `company #${numeric}` };
|
|
21064
|
+
} else {
|
|
21065
|
+
return usage2(
|
|
21066
|
+
`${options.to} is not one of your companies. For another company, pass its company id (an admin of that company finds it in its settings).`
|
|
21067
|
+
);
|
|
21068
|
+
}
|
|
21069
|
+
const mod = await resolveModule(slug, s, json);
|
|
21070
|
+
if (typeof mod === "number") return mod;
|
|
21071
|
+
const stop = await confirm(
|
|
21072
|
+
[
|
|
21073
|
+
`Invite ${target.label} to install ${mod.slug}?`,
|
|
21074
|
+
`- Admins of that company can then see ${mod.slug} and install it.`,
|
|
21075
|
+
"- Their data stays theirs; you do not see it.",
|
|
21076
|
+
`- Your module's logic and its updates reach their installations.`,
|
|
21077
|
+
"- You see their company name only after one of their admins installs it or asks to.",
|
|
21078
|
+
`- You can withdraw the invitation at any time with ${code(`dashwise module invites revoke ${mod.slug} <invitation id>`)}.`
|
|
21079
|
+
].join("\n"),
|
|
21080
|
+
"Send the invitation",
|
|
21081
|
+
options
|
|
21082
|
+
);
|
|
21083
|
+
if (stop !== null) return stop;
|
|
21084
|
+
let invite;
|
|
21085
|
+
try {
|
|
21086
|
+
const body = await apiRequest(
|
|
21087
|
+
"POST",
|
|
21088
|
+
`/api/modules/${mod.id}/invites`,
|
|
21089
|
+
{ workspaceId: target.id, ...options.note !== void 0 ? { note: options.note } : {} },
|
|
21090
|
+
s.auth
|
|
21091
|
+
);
|
|
21092
|
+
invite = body.invite;
|
|
21093
|
+
} catch (err) {
|
|
21094
|
+
return reportRefusal(err, { module: mod.slug }, { json, action: "Sending the invitation" });
|
|
21095
|
+
}
|
|
21096
|
+
if (json) out(JSON.stringify({ module: mod.slug, invite }));
|
|
21097
|
+
else success(`Invited ${target.label} to install ${mod.slug} (invitation #${invite.id}).`);
|
|
21098
|
+
return 0;
|
|
21099
|
+
}
|
|
21100
|
+
async function moduleInvitesRevokeCommand(slug, inviteId, options) {
|
|
21101
|
+
const { json } = options;
|
|
21102
|
+
if (!positiveId2(inviteId)) {
|
|
21103
|
+
return reportCliError(`Pass the invitation id. List them with ${code(`dashwise module invites list ${slug}`)}.`, {
|
|
21104
|
+
json,
|
|
21105
|
+
code: "USAGE"
|
|
21106
|
+
});
|
|
21107
|
+
}
|
|
21108
|
+
const s = session(json);
|
|
21109
|
+
if (typeof s === "number") return s;
|
|
21110
|
+
const mod = await resolveModule(slug, s, json);
|
|
21111
|
+
if (typeof mod === "number") return mod;
|
|
21112
|
+
const stop = await confirm(
|
|
21113
|
+
[
|
|
21114
|
+
`Withdraw invitation #${inviteId} for ${mod.slug}?`,
|
|
21115
|
+
`- Installations made with it stay, but they no longer receive updates of ${mod.slug}.`,
|
|
21116
|
+
"- No new installation can start from it."
|
|
21117
|
+
].join("\n"),
|
|
21118
|
+
"Withdraw the invitation",
|
|
21119
|
+
options
|
|
21120
|
+
);
|
|
21121
|
+
if (stop !== null) return stop;
|
|
21122
|
+
try {
|
|
21123
|
+
await apiRequest("DELETE", `/api/modules/${mod.id}/invites/${inviteId}`, void 0, s.auth);
|
|
21124
|
+
} catch (err) {
|
|
21125
|
+
return reportRefusal(err, { module: mod.slug }, { json, action: "Withdrawing the invitation" });
|
|
21126
|
+
}
|
|
21127
|
+
if (json) out(JSON.stringify({ module: mod.slug, revoked: inviteId }));
|
|
21128
|
+
else success(`Withdrew invitation #${inviteId} for ${mod.slug}.`);
|
|
21129
|
+
return 0;
|
|
21130
|
+
}
|
|
21131
|
+
async function resolveOwnCompany(s, flag, opts) {
|
|
21132
|
+
const { json } = opts;
|
|
21133
|
+
let mine;
|
|
21134
|
+
try {
|
|
21135
|
+
mine = await apiRequest("GET", "/api/workspaces", void 0, s.auth);
|
|
21136
|
+
} catch (err) {
|
|
21137
|
+
return reportRefusal(err, {}, { json, action: "Reading your companies" });
|
|
21138
|
+
}
|
|
21139
|
+
const list = mine.map((w) => `${w.slug} (company #${w.id})`).join(", ");
|
|
21140
|
+
const wanted = flag ?? s.workspaceSlug;
|
|
21141
|
+
if (wanted !== void 0) {
|
|
21142
|
+
const found = mine.find((w) => w.slug === wanted || String(w.id) === wanted);
|
|
21143
|
+
if (found) return found;
|
|
21144
|
+
return reportCliError(`${wanted} is not one of your companies. Your companies: ${list}.`, { json, code: "USAGE" });
|
|
21145
|
+
}
|
|
21146
|
+
if (mine.length === 1) return mine[0];
|
|
21147
|
+
if (mine.length === 0) return reportCliError("Your account belongs to no company yet.", { json });
|
|
21148
|
+
if (!opts.interactive || json || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
21149
|
+
return reportCliError(`Choose the company with --workspace <slug or id>. Your companies: ${list}.`, {
|
|
21150
|
+
json,
|
|
21151
|
+
code: "USAGE"
|
|
21152
|
+
});
|
|
21153
|
+
}
|
|
21154
|
+
const choice = await select({
|
|
21155
|
+
message: "Which company?",
|
|
21156
|
+
options: mine.map((w) => ({ value: String(w.id), label: w.name, hint: w.slug }))
|
|
21157
|
+
});
|
|
21158
|
+
const picked = isCancel(choice) ? void 0 : mine.find((w) => String(w.id) === choice);
|
|
21159
|
+
if (!picked) {
|
|
21160
|
+
reportCliError("Aborted.", {});
|
|
21161
|
+
return 130;
|
|
21162
|
+
}
|
|
21163
|
+
return picked;
|
|
21164
|
+
}
|
|
21165
|
+
async function moduleInvitesReceivedCommand(options) {
|
|
21166
|
+
const { json } = options;
|
|
21167
|
+
const s = session(json);
|
|
21168
|
+
if (typeof s === "number") return s;
|
|
21169
|
+
const company = await resolveOwnCompany(s, options.workspace, { json, interactive: true });
|
|
21170
|
+
if (typeof company === "number") return company;
|
|
21171
|
+
let rows;
|
|
21172
|
+
try {
|
|
21173
|
+
rows = await apiRequest(
|
|
21174
|
+
"GET",
|
|
21175
|
+
`/api/modules/workspaces/${company.id}/invites`,
|
|
21176
|
+
void 0,
|
|
21177
|
+
s.auth
|
|
21178
|
+
);
|
|
21179
|
+
} catch (err) {
|
|
21180
|
+
return reportRefusal(err, {}, { json, action: "Reading the invitations" });
|
|
21181
|
+
}
|
|
21182
|
+
if (json) {
|
|
21183
|
+
out(JSON.stringify({ workspace: company.slug, invites: rows }));
|
|
21184
|
+
return 0;
|
|
21185
|
+
}
|
|
21186
|
+
if (rows.length === 0) {
|
|
21187
|
+
info(`No open invitations for ${code(company.slug)}.`);
|
|
21188
|
+
return 0;
|
|
21189
|
+
}
|
|
21190
|
+
for (const row of rows) {
|
|
21191
|
+
const note = row.note ? ` ${dim(`"${row.note}"`)}` : "";
|
|
21192
|
+
out(
|
|
21193
|
+
`#${row.id} ${row.module.slug} ${dim(`v${row.module.current_version}`)} from ${row.from_company.name} ${dim(row.invited_at.slice(0, 10))}${note}`
|
|
21194
|
+
);
|
|
21195
|
+
}
|
|
21196
|
+
const inCompany = `--workspace ${company.slug}`;
|
|
21197
|
+
info(
|
|
21198
|
+
`Install one with ${code(`dashwise module install <slug> ${inCompany}`)}, or decline it with ${code(`dashwise module invites decline <id> ${inCompany}`)}.`
|
|
21199
|
+
);
|
|
21200
|
+
return 0;
|
|
21201
|
+
}
|
|
21202
|
+
async function moduleInvitesDeclineCommand(inviteId, options) {
|
|
21203
|
+
const { json } = options;
|
|
21204
|
+
if (!positiveId2(inviteId)) {
|
|
21205
|
+
return reportCliError(`Pass the invitation id. List them with ${code("dashwise module invites received")}.`, {
|
|
21206
|
+
json,
|
|
21207
|
+
code: "USAGE"
|
|
21208
|
+
});
|
|
21209
|
+
}
|
|
21210
|
+
const s = session(json);
|
|
21211
|
+
if (typeof s === "number") return s;
|
|
21212
|
+
const company = await resolveOwnCompany(s, options.workspace, { json, interactive: !options.yes });
|
|
21213
|
+
if (typeof company === "number") return company;
|
|
21214
|
+
const stop = await confirm(
|
|
21215
|
+
`Decline invitation #${inviteId} for ${company.slug}? The module's company may invite you again.`,
|
|
21216
|
+
"Decline the invitation",
|
|
21217
|
+
options
|
|
21218
|
+
);
|
|
21219
|
+
if (stop !== null) return stop;
|
|
21220
|
+
try {
|
|
21221
|
+
await apiRequest(
|
|
21222
|
+
"POST",
|
|
21223
|
+
`/api/modules/workspaces/${company.id}/invites/${inviteId}/decline`,
|
|
21224
|
+
void 0,
|
|
21225
|
+
s.auth
|
|
21226
|
+
);
|
|
21227
|
+
} catch (err) {
|
|
21228
|
+
return reportRefusal(err, {}, { json, action: "Declining the invitation" });
|
|
21229
|
+
}
|
|
21230
|
+
if (json) out(JSON.stringify({ workspace: company.slug, declined: inviteId }));
|
|
21231
|
+
else success(`Declined invitation #${inviteId}.`);
|
|
21232
|
+
return 0;
|
|
21233
|
+
}
|
|
21234
|
+
|
|
20831
21235
|
// src/commands/module/pull-data.ts
|
|
20832
21236
|
init_api_client();
|
|
20833
21237
|
init_output();
|
|
@@ -24775,7 +25179,7 @@ async function moduleRuntimeSetCommand(slugArg, options) {
|
|
|
24775
25179
|
const slug = before.module.slug;
|
|
24776
25180
|
const subject = { module: slug, installationId: before.installation_id };
|
|
24777
25181
|
if (streaming !== void 0 && !options.yes && !options.json && process.stdin.isTTY) {
|
|
24778
|
-
const ok = await confirm({
|
|
25182
|
+
const ok = await confirm$1({
|
|
24779
25183
|
message: `Turn streaming ${streaming === "RESPONSE_STREAM" ? "on" : "off"} for ${slug} (install #${before.installation_id})? The switch takes about a minute, and pages may come back empty while it runs.`
|
|
24780
25184
|
});
|
|
24781
25185
|
if (isCancel(ok) || ok !== true) {
|
|
@@ -25695,6 +26099,63 @@ moduleCmd.command("uninstall [slug]").description(
|
|
|
25695
26099
|
});
|
|
25696
26100
|
}
|
|
25697
26101
|
);
|
|
26102
|
+
var invitesCmd = moduleCmd.command("invites").description(
|
|
26103
|
+
"Install invitations: send, list and withdraw them for your module, or see and decline the ones your company received."
|
|
26104
|
+
);
|
|
26105
|
+
var inviteIdArg = (value) => value !== void 0 && /^[1-9][0-9]*$/.test(value) ? Number(value) : void 0;
|
|
26106
|
+
invitesCmd.command("list <slug>").description("List the open install invitations for your module.").option("--json", "Emit machine-readable JSON.").action(async (slug, cmdOpts) => {
|
|
26107
|
+
process.exitCode = await moduleInvitesListCommand(slug, {
|
|
26108
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26109
|
+
});
|
|
26110
|
+
});
|
|
26111
|
+
invitesCmd.command("send <slug>").description(
|
|
26112
|
+
"Invite another company to install your module. Needs admin rights in the module's company. Asks before sending; pass --yes in scripts."
|
|
26113
|
+
).option("--to <company>", "The company id to invite, or the slug of a company you belong to.").option("--note <text>", "A short note shown to the invited company (max 1000 characters).").option("-y, --yes", "Send without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
|
|
26114
|
+
async (slug, cmdOpts) => {
|
|
26115
|
+
process.exitCode = await moduleInvitesSendCommand(slug, {
|
|
26116
|
+
...cmdOpts.to !== void 0 ? { to: cmdOpts.to } : {},
|
|
26117
|
+
...cmdOpts.note !== void 0 ? { note: cmdOpts.note } : {},
|
|
26118
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
|
|
26119
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26120
|
+
});
|
|
26121
|
+
}
|
|
26122
|
+
);
|
|
26123
|
+
invitesCmd.command("revoke <slug> <invite-id>").description(
|
|
26124
|
+
"Withdraw an install invitation. Installations made before stay. Asks first; pass --yes in scripts."
|
|
26125
|
+
).option("-y, --yes", "Withdraw without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(async (slug, inviteId, cmdOpts) => {
|
|
26126
|
+
process.exitCode = await moduleInvitesRevokeCommand(slug, inviteIdArg(inviteId), {
|
|
26127
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
|
|
26128
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26129
|
+
});
|
|
26130
|
+
});
|
|
26131
|
+
invitesCmd.command("received").description("List the open install invitations your company received (company admins).").option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("--json", "Emit machine-readable JSON.").action(async (cmdOpts) => {
|
|
26132
|
+
process.exitCode = await moduleInvitesReceivedCommand({
|
|
26133
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
26134
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26135
|
+
});
|
|
26136
|
+
});
|
|
26137
|
+
invitesCmd.command("decline <invite-id>").description(
|
|
26138
|
+
"Decline an install invitation your company received (company admins). Asks first; pass --yes in scripts."
|
|
26139
|
+
).option("--workspace <slug-or-id>", "Company (default: config default or interactive picker).").option("-y, --yes", "Decline without asking (required in non-interactive or --json mode).").option("--json", "Emit machine-readable JSON.").action(
|
|
26140
|
+
async (inviteId, cmdOpts) => {
|
|
26141
|
+
process.exitCode = await moduleInvitesDeclineCommand(inviteIdArg(inviteId), {
|
|
26142
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
26143
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
|
|
26144
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26145
|
+
});
|
|
26146
|
+
}
|
|
26147
|
+
);
|
|
26148
|
+
moduleCmd.command("installations [slug]").description(
|
|
26149
|
+
"List the installations in a company that you can see: id, module, name, audience, version, held state and public address. With a slug, only that module. Read-only."
|
|
26150
|
+
).option("--workspace <slug-or-id>", "Company (default: cached module company, config default, or interactive picker).").option("--dev", "Include development installations.").option("--json", "Emit machine-readable JSON.").action(
|
|
26151
|
+
async (slug, cmdOpts) => {
|
|
26152
|
+
process.exitCode = await moduleInstallationsCommand(slug, {
|
|
26153
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
26154
|
+
...cmdOpts.dev !== void 0 ? { dev: cmdOpts.dev } : {},
|
|
26155
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
26156
|
+
});
|
|
26157
|
+
}
|
|
26158
|
+
);
|
|
25698
26159
|
moduleCmd.command("status [slug]").description(
|
|
25699
26160
|
"Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
|
|
25700
26161
|
).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
|
|
@@ -25972,7 +26433,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
25972
26433
|
}
|
|
25973
26434
|
);
|
|
25974
26435
|
function getVersion() {
|
|
25975
|
-
return "0.
|
|
26436
|
+
return "0.27.0";
|
|
25976
26437
|
}
|
|
25977
26438
|
function parseIntOption(value) {
|
|
25978
26439
|
const n = Number.parseInt(value, 10);
|