@dashai/cli 0.4.0 → 0.5.1
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 +78 -6
- package/dist/bin.js +595 -12
- package/dist/bin.js.map +1 -1
- package/package.json +2 -3
package/dist/bin.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir, platform, homedir } from 'os';
|
|
|
4
4
|
import { resolve, join, dirname, relative } from 'path';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { intro, log, select, isCancel, cancel, spinner, outro, text, multiselect, confirm } from '@clack/prompts';
|
|
7
|
-
import { Command } from 'commander';
|
|
7
|
+
import { Command, Option } from 'commander';
|
|
8
8
|
import { spawn, spawnSync } from 'child_process';
|
|
9
9
|
import open from 'open';
|
|
10
10
|
import * as tar from 'tar';
|
|
@@ -231,6 +231,30 @@ async function destroyDevInstallation(installationId, options = {}) {
|
|
|
231
231
|
options
|
|
232
232
|
);
|
|
233
233
|
}
|
|
234
|
+
async function createApiKey(installationId, body, options = {}) {
|
|
235
|
+
return apiRequest(
|
|
236
|
+
"POST",
|
|
237
|
+
`/api/installations/${installationId}/api-keys`,
|
|
238
|
+
body,
|
|
239
|
+
options
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
async function listApiKeys(installationId, options = {}) {
|
|
243
|
+
return apiRequest(
|
|
244
|
+
"GET",
|
|
245
|
+
`/api/installations/${installationId}/api-keys`,
|
|
246
|
+
void 0,
|
|
247
|
+
options
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
async function revokeApiKey(installationId, keyId, options = {}) {
|
|
251
|
+
return apiRequest(
|
|
252
|
+
"DELETE",
|
|
253
|
+
`/api/installations/${installationId}/api-keys/${keyId}`,
|
|
254
|
+
void 0,
|
|
255
|
+
options
|
|
256
|
+
);
|
|
257
|
+
}
|
|
234
258
|
async function createTable(installationId, body, options = {}) {
|
|
235
259
|
return apiRequest(
|
|
236
260
|
"POST",
|
|
@@ -370,6 +394,9 @@ var init_output = __esm({
|
|
|
370
394
|
});
|
|
371
395
|
|
|
372
396
|
// src/lib/scaffold.ts
|
|
397
|
+
function isDeployTarget(value) {
|
|
398
|
+
return DEPLOY_TARGETS.includes(value);
|
|
399
|
+
}
|
|
373
400
|
function manifestJson(opts) {
|
|
374
401
|
const kind = opts.kind ?? "hand_authored";
|
|
375
402
|
const manifest = {
|
|
@@ -993,6 +1020,7 @@ function appApiAuthSignInTs() {
|
|
|
993
1020
|
// the DashWise FE \`/login?return_to=...\` automatically \u2014 you don't
|
|
994
1021
|
// need to handle the "user not logged in" case here.
|
|
995
1022
|
import { NextResponse } from 'next/server';
|
|
1023
|
+
import { getPublicOrigin } from '@/lib/request-origin';
|
|
996
1024
|
|
|
997
1025
|
export function GET(request: Request) {
|
|
998
1026
|
const url = new URL(request.url);
|
|
@@ -1018,8 +1046,10 @@ export function GET(request: Request) {
|
|
|
1018
1046
|
|
|
1019
1047
|
// Absolute URL for return_to so the backend knows which host to
|
|
1020
1048
|
// bounce back to (the callback path is fixed at /api/auth/callback
|
|
1021
|
-
// by SDK convention).
|
|
1022
|
-
|
|
1049
|
+
// by SDK convention). \`getPublicOrigin\` reads \`X-Forwarded-Host\`
|
|
1050
|
+
// when present so the redirect URL points at the public domain,
|
|
1051
|
+
// not the internal Node origin behind reverse proxies.
|
|
1052
|
+
const moduleOrigin = getPublicOrigin(request);
|
|
1023
1053
|
const absoluteReturnTo = \`\${moduleOrigin}\${returnTo}\`;
|
|
1024
1054
|
|
|
1025
1055
|
let ssoStart;
|
|
@@ -1053,12 +1083,17 @@ function appApiAuthCallbackTs() {
|
|
|
1053
1083
|
// (e.g. \`access_denied\`, \`server_error\`, \`invalid_request\`).
|
|
1054
1084
|
import { NextResponse } from 'next/server';
|
|
1055
1085
|
import { exchangeCode } from '@dashai/sdk/auth/server';
|
|
1086
|
+
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1056
1087
|
|
|
1057
1088
|
export async function GET(request: Request) {
|
|
1058
1089
|
const url = new URL(request.url);
|
|
1090
|
+
// Resolve the public origin once \u2014 all redirects below are
|
|
1091
|
+
// back to this module, and behind a reverse proxy url.origin is
|
|
1092
|
+
// the internal Node origin (localhost:<PORT>), not the public host.
|
|
1093
|
+
const publicOrigin = getPublicOrigin(request);
|
|
1059
1094
|
const error = url.searchParams.get('error');
|
|
1060
1095
|
if (error) {
|
|
1061
|
-
const target = new URL('/sign-in-error',
|
|
1096
|
+
const target = new URL('/sign-in-error', publicOrigin);
|
|
1062
1097
|
target.searchParams.set('code', error);
|
|
1063
1098
|
const description = url.searchParams.get('error_description');
|
|
1064
1099
|
if (description) target.searchParams.set('description', description);
|
|
@@ -1067,19 +1102,19 @@ export async function GET(request: Request) {
|
|
|
1067
1102
|
const code = url.searchParams.get('code');
|
|
1068
1103
|
const returnTo = url.searchParams.get('return_to') ?? '/';
|
|
1069
1104
|
if (!code) {
|
|
1070
|
-
const target = new URL('/sign-in-error',
|
|
1105
|
+
const target = new URL('/sign-in-error', publicOrigin);
|
|
1071
1106
|
target.searchParams.set('code', 'missing_code');
|
|
1072
1107
|
return NextResponse.redirect(target);
|
|
1073
1108
|
}
|
|
1074
1109
|
try {
|
|
1075
1110
|
await exchangeCode(code); // sets the dashwise.session cookie
|
|
1076
1111
|
} catch (err) {
|
|
1077
|
-
const target = new URL('/sign-in-error',
|
|
1112
|
+
const target = new URL('/sign-in-error', publicOrigin);
|
|
1078
1113
|
target.searchParams.set('code', 'exchange_failed');
|
|
1079
1114
|
target.searchParams.set('description', (err as Error).message);
|
|
1080
1115
|
return NextResponse.redirect(target);
|
|
1081
1116
|
}
|
|
1082
|
-
return NextResponse.redirect(new URL(returnTo,
|
|
1117
|
+
return NextResponse.redirect(new URL(returnTo, publicOrigin));
|
|
1083
1118
|
}
|
|
1084
1119
|
`;
|
|
1085
1120
|
}
|
|
@@ -1092,10 +1127,14 @@ function appApiAuthSignOutTs() {
|
|
|
1092
1127
|
// to follow with GET (not re-POST), landing on the public '/' page.
|
|
1093
1128
|
import { NextResponse } from 'next/server';
|
|
1094
1129
|
import { signOut } from '@dashai/sdk/auth/server';
|
|
1130
|
+
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1095
1131
|
|
|
1096
1132
|
export async function POST(request: Request) {
|
|
1097
1133
|
await signOut();
|
|
1098
|
-
|
|
1134
|
+
// Build the redirect against the public origin so we don't land
|
|
1135
|
+
// the user on \`http://localhost:<PORT>/\` when running behind a
|
|
1136
|
+
// reverse proxy (Railway/Vercel/Fly).
|
|
1137
|
+
return NextResponse.redirect(new URL('/', getPublicOrigin(request)), 303);
|
|
1099
1138
|
}
|
|
1100
1139
|
`;
|
|
1101
1140
|
}
|
|
@@ -1269,6 +1308,36 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
1269
1308
|
}
|
|
1270
1309
|
`;
|
|
1271
1310
|
}
|
|
1311
|
+
function libRequestOriginTs() {
|
|
1312
|
+
return `/**
|
|
1313
|
+
* Resolve the **public** origin of an incoming request.
|
|
1314
|
+
*
|
|
1315
|
+
* Behind a reverse proxy (Railway, Vercel, Fly, Cloudflare, an
|
|
1316
|
+
* nginx in front of a Node container, etc.) \`new URL(request.url)\`
|
|
1317
|
+
* reports the *internal* origin the Node process is bound to \u2014
|
|
1318
|
+
* usually \`http://localhost:<PORT>\` \u2014 not the public URL the
|
|
1319
|
+
* browser hit. The proxy preserves the public origin in
|
|
1320
|
+
* \`X-Forwarded-*\` headers; we read those first.
|
|
1321
|
+
*
|
|
1322
|
+
* Falls back to \`new URL(request.url).origin\` for local dev where
|
|
1323
|
+
* there's no proxy in front of Next.js.
|
|
1324
|
+
*
|
|
1325
|
+
* Both \`x-forwarded-proto\` and \`x-forwarded-host\` can be
|
|
1326
|
+
* comma-separated when multiple proxies chain; the leftmost entry
|
|
1327
|
+
* is the original client-facing one per RFC 7239 conventions.
|
|
1328
|
+
*/
|
|
1329
|
+
export function getPublicOrigin(request: Request): string {
|
|
1330
|
+
const forwardedProto = request.headers.get('x-forwarded-proto');
|
|
1331
|
+
const forwardedHost = request.headers.get('x-forwarded-host');
|
|
1332
|
+
if (forwardedProto && forwardedHost) {
|
|
1333
|
+
const proto = forwardedProto.split(',')[0].trim();
|
|
1334
|
+
const host = forwardedHost.split(',')[0].trim();
|
|
1335
|
+
return \`\${proto}://\${host}\`;
|
|
1336
|
+
}
|
|
1337
|
+
return new URL(request.url).origin;
|
|
1338
|
+
}
|
|
1339
|
+
`;
|
|
1340
|
+
}
|
|
1272
1341
|
function componentsUiButtonTsx() {
|
|
1273
1342
|
return `import * as React from "react";
|
|
1274
1343
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
@@ -1726,9 +1795,63 @@ Format (reverse chronological, newest first):
|
|
|
1726
1795
|
schema mutation.
|
|
1727
1796
|
`;
|
|
1728
1797
|
}
|
|
1729
|
-
|
|
1798
|
+
function railwayJson(_opts) {
|
|
1799
|
+
return JSON.stringify(
|
|
1800
|
+
{
|
|
1801
|
+
$schema: "https://railway.com/railway.schema.json",
|
|
1802
|
+
build: {
|
|
1803
|
+
builder: "NIXPACKS"
|
|
1804
|
+
},
|
|
1805
|
+
deploy: {
|
|
1806
|
+
restartPolicyType: "ON_FAILURE",
|
|
1807
|
+
restartPolicyMaxRetries: 3
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
null,
|
|
1811
|
+
2
|
|
1812
|
+
) + "\n";
|
|
1813
|
+
}
|
|
1814
|
+
function vercelJson(_opts) {
|
|
1815
|
+
return JSON.stringify(
|
|
1816
|
+
{
|
|
1817
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
1818
|
+
framework: "nextjs"
|
|
1819
|
+
},
|
|
1820
|
+
null,
|
|
1821
|
+
2
|
|
1822
|
+
) + "\n";
|
|
1823
|
+
}
|
|
1824
|
+
function flyToml(opts) {
|
|
1825
|
+
return `# fly.toml \u2014 generated by \`dashwise init --deploy-target fly\`.
|
|
1826
|
+
# Run \`fly launch --no-deploy --copy-config\` to register this app with
|
|
1827
|
+
# Fly; the launcher will read this file and prompt for anything missing.
|
|
1828
|
+
#
|
|
1829
|
+
# Edit \`app\` if the slug is already taken globally on fly.io.
|
|
1830
|
+
# Edit \`primary_region\` for non-US-East deploys (https://fly.io/docs/reference/regions/).
|
|
1831
|
+
|
|
1832
|
+
app = "${opts.slug}"
|
|
1833
|
+
primary_region = "iad"
|
|
1834
|
+
|
|
1835
|
+
[build]
|
|
1836
|
+
|
|
1837
|
+
[http_service]
|
|
1838
|
+
internal_port = 3000
|
|
1839
|
+
force_https = true
|
|
1840
|
+
auto_stop_machines = "stop"
|
|
1841
|
+
auto_start_machines = true
|
|
1842
|
+
min_machines_running = 0
|
|
1843
|
+
processes = ["app"]
|
|
1844
|
+
|
|
1845
|
+
[[vm]]
|
|
1846
|
+
cpu_kind = "shared"
|
|
1847
|
+
cpus = 1
|
|
1848
|
+
memory_mb = 256
|
|
1849
|
+
`;
|
|
1850
|
+
}
|
|
1851
|
+
var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL;
|
|
1730
1852
|
var init_scaffold = __esm({
|
|
1731
1853
|
"src/lib/scaffold.ts"() {
|
|
1854
|
+
DEPLOY_TARGETS = ["railway", "vercel", "fly"];
|
|
1732
1855
|
SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
|
|
1733
1856
|
}
|
|
1734
1857
|
});
|
|
@@ -1830,6 +1953,12 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1830
1953
|
);
|
|
1831
1954
|
return 1;
|
|
1832
1955
|
}
|
|
1956
|
+
if (options.deployTarget !== void 0 && !isDeployTarget(options.deployTarget)) {
|
|
1957
|
+
fail(
|
|
1958
|
+
`Invalid --deploy-target ${code(options.deployTarget)} \u2014 must be one of ${DEPLOY_TARGETS.map((t) => code(t)).join(", ")}.`
|
|
1959
|
+
);
|
|
1960
|
+
return 1;
|
|
1961
|
+
}
|
|
1833
1962
|
const kind = options.simple ? "hand_authored" : "custom";
|
|
1834
1963
|
const scaffoldOpts = {
|
|
1835
1964
|
slug,
|
|
@@ -1862,6 +1991,7 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1862
1991
|
["postcss.config.mjs", postcssConfigMjs()],
|
|
1863
1992
|
["app/globals.css", globalsCss()],
|
|
1864
1993
|
["lib/utils.ts", libUtilsTs()],
|
|
1994
|
+
["lib/request-origin.ts", libRequestOriginTs()],
|
|
1865
1995
|
["components/theme-provider.tsx", componentsThemeProviderTsx()],
|
|
1866
1996
|
["components/ui/button.tsx", componentsUiButtonTsx()],
|
|
1867
1997
|
["components/ui/card.tsx", componentsUiCardTsx()],
|
|
@@ -1880,6 +2010,19 @@ async function moduleInitCommand(slugArg, options) {
|
|
|
1880
2010
|
["app/page.tsx", appPageTsx(scaffoldOpts)]
|
|
1881
2011
|
);
|
|
1882
2012
|
}
|
|
2013
|
+
if (options.deployTarget !== void 0) {
|
|
2014
|
+
switch (options.deployTarget) {
|
|
2015
|
+
case "railway":
|
|
2016
|
+
files.push(["railway.json", railwayJson()]);
|
|
2017
|
+
break;
|
|
2018
|
+
case "vercel":
|
|
2019
|
+
files.push(["vercel.json", vercelJson()]);
|
|
2020
|
+
break;
|
|
2021
|
+
case "fly":
|
|
2022
|
+
files.push(["fly.toml", flyToml(scaffoldOpts)]);
|
|
2023
|
+
break;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
1883
2026
|
for (const [relPath, content] of files) {
|
|
1884
2027
|
const absPath = join(dir, relPath);
|
|
1885
2028
|
const parentDir = join(absPath, "..");
|
|
@@ -2912,7 +3055,7 @@ async function devCommand(options) {
|
|
|
2912
3055
|
pipeWithPrefix(child.stderr, process.stderr, prefix);
|
|
2913
3056
|
}
|
|
2914
3057
|
let manifestPoll = null;
|
|
2915
|
-
const enablePoll = !options.noRemotePoll && moduleEntry !==
|
|
3058
|
+
const enablePoll = !options.noRemotePoll && moduleEntry !== null && manifestSlug !== void 0;
|
|
2916
3059
|
if (enablePoll) {
|
|
2917
3060
|
const intervalSec = Math.max(
|
|
2918
3061
|
MIN_REMOTE_POLL_INTERVAL_SEC,
|
|
@@ -5219,6 +5362,374 @@ function surfacePullFailure(err) {
|
|
|
5219
5362
|
return 1;
|
|
5220
5363
|
}
|
|
5221
5364
|
|
|
5365
|
+
// src/bin.ts
|
|
5366
|
+
init_scaffold();
|
|
5367
|
+
|
|
5368
|
+
// src/commands/api-keys.ts
|
|
5369
|
+
init_api_client();
|
|
5370
|
+
init_config();
|
|
5371
|
+
init_manifest_io();
|
|
5372
|
+
init_output();
|
|
5373
|
+
function loadContext(opts) {
|
|
5374
|
+
const manifest = readManifestOrThrow(opts.dir);
|
|
5375
|
+
const slug = manifest.module?.slug;
|
|
5376
|
+
if (!slug) {
|
|
5377
|
+
throw new Error(
|
|
5378
|
+
`module.json has no module.slug \u2014 the manifest is malformed.`
|
|
5379
|
+
);
|
|
5380
|
+
}
|
|
5381
|
+
const cfg = readConfig();
|
|
5382
|
+
if (!cfg) {
|
|
5383
|
+
throw new Error("Not logged in. Run `dashwise login` first.");
|
|
5384
|
+
}
|
|
5385
|
+
const entry = readModuleEntry(slug);
|
|
5386
|
+
if (!entry) {
|
|
5387
|
+
throw new Error(
|
|
5388
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
5389
|
+
);
|
|
5390
|
+
}
|
|
5391
|
+
return {
|
|
5392
|
+
installationId: entry.installationId,
|
|
5393
|
+
slug,
|
|
5394
|
+
requestOptions: {
|
|
5395
|
+
baseUrl: resolveApiUrl(),
|
|
5396
|
+
auth: { token: cfg.token }
|
|
5397
|
+
}
|
|
5398
|
+
};
|
|
5399
|
+
}
|
|
5400
|
+
async function apiKeysListCommand(options) {
|
|
5401
|
+
let ctx;
|
|
5402
|
+
try {
|
|
5403
|
+
ctx = loadContext(options);
|
|
5404
|
+
} catch (err) {
|
|
5405
|
+
fail(err.message);
|
|
5406
|
+
return 1;
|
|
5407
|
+
}
|
|
5408
|
+
let keys;
|
|
5409
|
+
try {
|
|
5410
|
+
const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
|
|
5411
|
+
keys = resp.api_keys;
|
|
5412
|
+
} catch (err) {
|
|
5413
|
+
return surfaceApiKeysError(err);
|
|
5414
|
+
}
|
|
5415
|
+
const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
|
|
5416
|
+
if (options.json) {
|
|
5417
|
+
process.stdout.write(`${JSON.stringify(filtered, null, 2)}
|
|
5418
|
+
`);
|
|
5419
|
+
return 0;
|
|
5420
|
+
}
|
|
5421
|
+
if (filtered.length === 0) {
|
|
5422
|
+
info(
|
|
5423
|
+
options.all ? `No API keys (active or revoked) for ${code(ctx.slug)}.` : `No active API keys for ${code(ctx.slug)}. Run ${code("dashwise api-keys create <name>")} to mint one, or ${code("dashwise api-keys list --all")} to include revoked.`
|
|
5424
|
+
);
|
|
5425
|
+
return 0;
|
|
5426
|
+
}
|
|
5427
|
+
info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
|
|
5428
|
+
for (const k of filtered) {
|
|
5429
|
+
const status = formatKeyStatus(k);
|
|
5430
|
+
const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
|
|
5431
|
+
process.stdout.write(
|
|
5432
|
+
` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
|
|
5433
|
+
`
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
5436
|
+
return 0;
|
|
5437
|
+
}
|
|
5438
|
+
async function apiKeysCreateCommand(nameArg, options) {
|
|
5439
|
+
const name = nameArg ?? options.name;
|
|
5440
|
+
if (!name) {
|
|
5441
|
+
fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
|
|
5442
|
+
return 1;
|
|
5443
|
+
}
|
|
5444
|
+
let ctx;
|
|
5445
|
+
try {
|
|
5446
|
+
ctx = loadContext(options);
|
|
5447
|
+
} catch (err) {
|
|
5448
|
+
fail(err.message);
|
|
5449
|
+
return 1;
|
|
5450
|
+
}
|
|
5451
|
+
const body = { name };
|
|
5452
|
+
if (options.scopes !== void 0) {
|
|
5453
|
+
body.scopes = options.scopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
5454
|
+
}
|
|
5455
|
+
if (options.expiresAt !== void 0) {
|
|
5456
|
+
body.expires_at = options.expiresAt;
|
|
5457
|
+
}
|
|
5458
|
+
let resp;
|
|
5459
|
+
try {
|
|
5460
|
+
resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
|
|
5461
|
+
} catch (err) {
|
|
5462
|
+
return surfaceApiKeysError(err);
|
|
5463
|
+
}
|
|
5464
|
+
if (options.rawOnly) {
|
|
5465
|
+
process.stderr.write(
|
|
5466
|
+
`Created API key #${resp.api_key.id} (${resp.api_key.name}) for install ${ctx.installationId}.
|
|
5467
|
+
`
|
|
5468
|
+
);
|
|
5469
|
+
process.stdout.write(`${resp.raw_key}
|
|
5470
|
+
`);
|
|
5471
|
+
return 0;
|
|
5472
|
+
}
|
|
5473
|
+
success(
|
|
5474
|
+
`Created API key ${code(`#${resp.api_key.id}`)} (${resp.api_key.name}) for ${code(ctx.slug)}.`
|
|
5475
|
+
);
|
|
5476
|
+
info("");
|
|
5477
|
+
warn(
|
|
5478
|
+
"This is the ONLY time the full key is displayed. Save it now \u2014 the backend stores a hash, not the plaintext."
|
|
5479
|
+
);
|
|
5480
|
+
info("");
|
|
5481
|
+
process.stdout.write(` ${code(resp.raw_key)}
|
|
5482
|
+
`);
|
|
5483
|
+
info("");
|
|
5484
|
+
info(`Use in a deployed module's env:`);
|
|
5485
|
+
info(` ${code(`DASHWISE_API_KEY=${resp.raw_key.slice(0, 12)}\u2026`)}`);
|
|
5486
|
+
info(
|
|
5487
|
+
` ${code(`DASHWISE_INSTALLATION_ID=${ctx.installationId}`)}`
|
|
5488
|
+
);
|
|
5489
|
+
info(
|
|
5490
|
+
` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`
|
|
5491
|
+
);
|
|
5492
|
+
return 0;
|
|
5493
|
+
}
|
|
5494
|
+
async function apiKeysRevokeCommand(keyIdArg, options) {
|
|
5495
|
+
if (!keyIdArg) {
|
|
5496
|
+
fail("Usage: `dashwise api-keys revoke <key-id>` \u2014 id from `dashwise api-keys list`");
|
|
5497
|
+
return 1;
|
|
5498
|
+
}
|
|
5499
|
+
const keyId = Number.parseInt(keyIdArg, 10);
|
|
5500
|
+
if (!Number.isFinite(keyId) || keyId <= 0) {
|
|
5501
|
+
fail(`Invalid key id "${keyIdArg}" \u2014 expected a positive integer.`);
|
|
5502
|
+
return 1;
|
|
5503
|
+
}
|
|
5504
|
+
let ctx;
|
|
5505
|
+
try {
|
|
5506
|
+
ctx = loadContext(options);
|
|
5507
|
+
} catch (err) {
|
|
5508
|
+
fail(err.message);
|
|
5509
|
+
return 1;
|
|
5510
|
+
}
|
|
5511
|
+
if (!options.yes) {
|
|
5512
|
+
info(
|
|
5513
|
+
`Revoking API key #${keyId} for ${code(ctx.slug)} (install ${ctx.installationId}). Pass ${code("--yes")} to skip this confirmation in scripts.`
|
|
5514
|
+
);
|
|
5515
|
+
}
|
|
5516
|
+
let resp;
|
|
5517
|
+
try {
|
|
5518
|
+
resp = await revokeApiKey(ctx.installationId, keyId, ctx.requestOptions);
|
|
5519
|
+
} catch (err) {
|
|
5520
|
+
return surfaceApiKeysError(err);
|
|
5521
|
+
}
|
|
5522
|
+
success(
|
|
5523
|
+
`Revoked API key #${resp.api_key.id} (${resp.api_key.name}). Future requests with this key will return 401.`
|
|
5524
|
+
);
|
|
5525
|
+
return 0;
|
|
5526
|
+
}
|
|
5527
|
+
function surfaceApiKeysError(err) {
|
|
5528
|
+
if (err instanceof ApiError) {
|
|
5529
|
+
switch (err.code) {
|
|
5530
|
+
case "UNAUTHENTICATED":
|
|
5531
|
+
fail(
|
|
5532
|
+
`Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
|
|
5533
|
+
);
|
|
5534
|
+
return 1;
|
|
5535
|
+
case "NOT_WORKSPACE_MEMBER":
|
|
5536
|
+
fail(
|
|
5537
|
+
`You're not a member of this installation's workspace. API-key management requires workspace membership.`
|
|
5538
|
+
);
|
|
5539
|
+
return 1;
|
|
5540
|
+
case "INSTALLATION_NOT_FOUND":
|
|
5541
|
+
fail(
|
|
5542
|
+
`Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
|
|
5543
|
+
);
|
|
5544
|
+
return 1;
|
|
5545
|
+
case "API_KEY_NOT_FOUND":
|
|
5546
|
+
fail(
|
|
5547
|
+
`API key not found for this installation. Use ${code("dashwise api-keys list")} to see valid ids.`
|
|
5548
|
+
);
|
|
5549
|
+
return 1;
|
|
5550
|
+
default:
|
|
5551
|
+
fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
5552
|
+
return 1;
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
fail(`API-key operation failed: ${err.message}`);
|
|
5556
|
+
return 1;
|
|
5557
|
+
}
|
|
5558
|
+
function formatKeyStatus(k) {
|
|
5559
|
+
if (k.revoked_at) return dim("[revoked]");
|
|
5560
|
+
if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) {
|
|
5561
|
+
return dim("[expired]");
|
|
5562
|
+
}
|
|
5563
|
+
return dim("[active]");
|
|
5564
|
+
}
|
|
5565
|
+
function formatRelative(d) {
|
|
5566
|
+
const deltaMs = Date.now() - d.getTime();
|
|
5567
|
+
const sec = Math.floor(deltaMs / 1e3);
|
|
5568
|
+
if (sec < 60) return `${sec}s ago`;
|
|
5569
|
+
const min = Math.floor(sec / 60);
|
|
5570
|
+
if (min < 60) return `${min}m ago`;
|
|
5571
|
+
const hr = Math.floor(min / 60);
|
|
5572
|
+
if (hr < 24) return `${hr}h ago`;
|
|
5573
|
+
const day = Math.floor(hr / 24);
|
|
5574
|
+
return `${day}d ago`;
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
// src/commands/env-export.ts
|
|
5578
|
+
init_api_client();
|
|
5579
|
+
init_config();
|
|
5580
|
+
init_manifest_io();
|
|
5581
|
+
init_output();
|
|
5582
|
+
var PLACEHOLDER = "<run-`dashwise-api-keys-create-<name>`-and-paste-here>";
|
|
5583
|
+
async function envExportCommand(options) {
|
|
5584
|
+
const format = options.format ?? "env";
|
|
5585
|
+
if (!["env", "json", "railway", "vercel", "fly"].includes(format)) {
|
|
5586
|
+
fail(
|
|
5587
|
+
`Unknown --format "${format}". Allowed: env | json | railway | vercel | fly.`
|
|
5588
|
+
);
|
|
5589
|
+
return 1;
|
|
5590
|
+
}
|
|
5591
|
+
if (options.newKey && options.key) {
|
|
5592
|
+
fail(
|
|
5593
|
+
`Pass either --new-key (mint fresh) or --key (paste existing). Not both.`
|
|
5594
|
+
);
|
|
5595
|
+
return 1;
|
|
5596
|
+
}
|
|
5597
|
+
let manifest;
|
|
5598
|
+
try {
|
|
5599
|
+
manifest = readManifestOrThrow(options.dir);
|
|
5600
|
+
} catch (err) {
|
|
5601
|
+
fail(err.message);
|
|
5602
|
+
return 1;
|
|
5603
|
+
}
|
|
5604
|
+
const slug = manifest.module?.slug;
|
|
5605
|
+
if (!slug) {
|
|
5606
|
+
fail("module.json has no module.slug \u2014 the manifest is malformed.");
|
|
5607
|
+
return 1;
|
|
5608
|
+
}
|
|
5609
|
+
const cfg = readConfig();
|
|
5610
|
+
if (!cfg) {
|
|
5611
|
+
fail("Not logged in. Run `dashwise login` first.");
|
|
5612
|
+
return 1;
|
|
5613
|
+
}
|
|
5614
|
+
const entry = readModuleEntry(slug);
|
|
5615
|
+
if (!entry) {
|
|
5616
|
+
fail(
|
|
5617
|
+
`No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
|
|
5618
|
+
);
|
|
5619
|
+
return 1;
|
|
5620
|
+
}
|
|
5621
|
+
const apiUrl = resolveApiUrl();
|
|
5622
|
+
const installationId = entry.installationId;
|
|
5623
|
+
let apiKey;
|
|
5624
|
+
let apiKeyComment = null;
|
|
5625
|
+
if (options.key !== void 0) {
|
|
5626
|
+
if (!options.key.startsWith("dwk_")) {
|
|
5627
|
+
fail(`--key value must start with \`dwk_\` (got "${options.key.slice(0, 8)}\u2026").`);
|
|
5628
|
+
return 1;
|
|
5629
|
+
}
|
|
5630
|
+
apiKey = options.key;
|
|
5631
|
+
apiKeyComment = "# DASHWISE_API_KEY supplied via --key";
|
|
5632
|
+
} else if (options.newKey !== void 0) {
|
|
5633
|
+
try {
|
|
5634
|
+
const resp = await createApiKey(
|
|
5635
|
+
installationId,
|
|
5636
|
+
{ name: options.newKey },
|
|
5637
|
+
{
|
|
5638
|
+
baseUrl: apiUrl,
|
|
5639
|
+
auth: { token: cfg.token }
|
|
5640
|
+
}
|
|
5641
|
+
);
|
|
5642
|
+
apiKey = resp.raw_key;
|
|
5643
|
+
apiKeyComment = `# DASHWISE_API_KEY minted by --new-key (id=${resp.api_key.id}, name="${resp.api_key.name}")`;
|
|
5644
|
+
process.stderr.write(
|
|
5645
|
+
`${dim("[env export]")} Minted API key #${resp.api_key.id} (${resp.api_key.name}). The plaintext below is the ONLY time it's shown \u2014 save the file or paste it now.
|
|
5646
|
+
`
|
|
5647
|
+
);
|
|
5648
|
+
} catch (err) {
|
|
5649
|
+
if (err instanceof ApiError) {
|
|
5650
|
+
fail(`Failed to mint API key: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
5651
|
+
} else {
|
|
5652
|
+
fail(`Failed to mint API key: ${err.message}`);
|
|
5653
|
+
}
|
|
5654
|
+
return 1;
|
|
5655
|
+
}
|
|
5656
|
+
} else {
|
|
5657
|
+
apiKey = PLACEHOLDER;
|
|
5658
|
+
apiKeyComment = "# DASHWISE_API_KEY \u2014 run `dashwise api-keys create <name>` to mint one + paste here.";
|
|
5659
|
+
}
|
|
5660
|
+
const entries = [
|
|
5661
|
+
{
|
|
5662
|
+
key: "DASHWISE_API_URL",
|
|
5663
|
+
value: apiUrl,
|
|
5664
|
+
comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
|
|
5665
|
+
},
|
|
5666
|
+
{
|
|
5667
|
+
key: "NEXT_PUBLIC_DASHWISE_API_URL",
|
|
5668
|
+
value: apiUrl,
|
|
5669
|
+
comment: "# Client-side mirror \u2014 Next.js inlines NEXT_PUBLIC_* into the browser bundle so <AuthProvider> hits the right origin."
|
|
5670
|
+
},
|
|
5671
|
+
{
|
|
5672
|
+
key: "DASHWISE_INSTALLATION_ID",
|
|
5673
|
+
value: String(installationId),
|
|
5674
|
+
comment: `# Per-workspace install id for module "${slug}".`
|
|
5675
|
+
},
|
|
5676
|
+
{
|
|
5677
|
+
key: "DASHWISE_API_KEY",
|
|
5678
|
+
value: apiKey,
|
|
5679
|
+
...apiKeyComment !== null ? { comment: apiKeyComment } : {}
|
|
5680
|
+
}
|
|
5681
|
+
];
|
|
5682
|
+
process.stdout.write(render(entries, format));
|
|
5683
|
+
if (format !== "json") process.stdout.write("\n");
|
|
5684
|
+
if (apiKey === PLACEHOLDER) {
|
|
5685
|
+
info("");
|
|
5686
|
+
info(
|
|
5687
|
+
`Next: ${code(`dashwise api-keys create <name>`)} \u2192 paste the plaintext into ${code("DASHWISE_API_KEY")}.`
|
|
5688
|
+
);
|
|
5689
|
+
} else if (options.newKey !== void 0) {
|
|
5690
|
+
info("");
|
|
5691
|
+
warn(
|
|
5692
|
+
"API key plaintext is shown ONLY once. Save the output of this command now."
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
return 0;
|
|
5696
|
+
}
|
|
5697
|
+
function render(entries, format) {
|
|
5698
|
+
switch (format) {
|
|
5699
|
+
case "env": {
|
|
5700
|
+
const lines = [];
|
|
5701
|
+
for (const e of entries) {
|
|
5702
|
+
if (e.comment) lines.push(e.comment);
|
|
5703
|
+
lines.push(`${e.key}=${e.value}`);
|
|
5704
|
+
lines.push("");
|
|
5705
|
+
}
|
|
5706
|
+
return lines.join("\n").trimEnd();
|
|
5707
|
+
}
|
|
5708
|
+
case "json": {
|
|
5709
|
+
const obj = {};
|
|
5710
|
+
for (const e of entries) obj[e.key] = e.value;
|
|
5711
|
+
return JSON.stringify(obj, null, 2);
|
|
5712
|
+
}
|
|
5713
|
+
case "railway": {
|
|
5714
|
+
const parts = entries.map((e) => `${e.key}='${e.value.replace(/'/g, "'\\''")}'`);
|
|
5715
|
+
return `railway variables set ${parts.join(" ")}`;
|
|
5716
|
+
}
|
|
5717
|
+
case "vercel": {
|
|
5718
|
+
const lines = entries.map(
|
|
5719
|
+
(e) => `echo ${shellQuote(e.value)} | vercel env add ${e.key} production`
|
|
5720
|
+
);
|
|
5721
|
+
return lines.join("\n");
|
|
5722
|
+
}
|
|
5723
|
+
case "fly": {
|
|
5724
|
+
const parts = entries.map((e) => `${e.key}=${shellQuote(e.value)}`);
|
|
5725
|
+
return `fly secrets set ${parts.join(" ")}`;
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
function shellQuote(value) {
|
|
5730
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5222
5733
|
// src/commands/query.ts
|
|
5223
5734
|
init_manifest_io();
|
|
5224
5735
|
init_output();
|
|
@@ -5864,6 +6375,11 @@ var initOptions = (cmd) => cmd.option("--dir <path>", "Override the target direc
|
|
|
5864
6375
|
).option(
|
|
5865
6376
|
"--no-provision",
|
|
5866
6377
|
"Skip the `POST /api/installations/dev` step. Local scaffold is still written; you can provision later by re-running `dashwise init --force` (or, once it ships, `dashwise module provision-dev-install`)."
|
|
6378
|
+
).addOption(
|
|
6379
|
+
new Option(
|
|
6380
|
+
"--deploy-target <platform>",
|
|
6381
|
+
"Emit a platform deploy config file alongside the scaffold (railway.json | vercel.json | fly.toml). Default: none (clean repo). Pair with `dashwise env export --format <platform>` to push env vars after provisioning."
|
|
6382
|
+
).choices([...DEPLOY_TARGETS])
|
|
5867
6383
|
);
|
|
5868
6384
|
var initAction = async (slugArg, cmdOpts) => {
|
|
5869
6385
|
process.exitCode = await moduleInitCommand(slugArg, {
|
|
@@ -5875,7 +6391,8 @@ var initAction = async (slugArg, cmdOpts) => {
|
|
|
5875
6391
|
...cmdOpts.git !== void 0 ? { git: cmdOpts.git } : {},
|
|
5876
6392
|
...cmdOpts.description !== void 0 ? { description: cmdOpts.description } : {},
|
|
5877
6393
|
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
5878
|
-
...cmdOpts.provision !== void 0 ? { provision: cmdOpts.provision } : {}
|
|
6394
|
+
...cmdOpts.provision !== void 0 ? { provision: cmdOpts.provision } : {},
|
|
6395
|
+
...cmdOpts.deployTarget !== void 0 ? { deployTarget: cmdOpts.deployTarget } : {}
|
|
5879
6396
|
});
|
|
5880
6397
|
};
|
|
5881
6398
|
initOptions(
|
|
@@ -6005,6 +6522,72 @@ program.command("pull").description(
|
|
|
6005
6522
|
...cmdOpts.dryRun !== void 0 ? { dryRun: cmdOpts.dryRun } : {}
|
|
6006
6523
|
});
|
|
6007
6524
|
});
|
|
6525
|
+
var apiKeysCmd = program.command("api-keys").description(
|
|
6526
|
+
"Manage per-installation API keys (`dwk_*`). Pasted into a deployed module's env for production data-plane auth."
|
|
6527
|
+
);
|
|
6528
|
+
apiKeysCmd.command("list").description(
|
|
6529
|
+
"List API keys for the current module's installation. Active only by default; pass --all for revoked too."
|
|
6530
|
+
).option("--dir <path>", "Project root (default: current directory).").option("--all", "Include revoked + expired keys in the output.").option("--json", "Emit JSON instead of the human-friendly table.").action(async (cmdOpts) => {
|
|
6531
|
+
process.exitCode = await apiKeysListCommand({
|
|
6532
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6533
|
+
...cmdOpts.all !== void 0 ? { all: cmdOpts.all } : {},
|
|
6534
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
6535
|
+
});
|
|
6536
|
+
});
|
|
6537
|
+
apiKeysCmd.command("create [name]").description(
|
|
6538
|
+
"Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
|
|
6539
|
+
).option("--dir <path>", "Project root (default: current directory).").option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
|
|
6540
|
+
"--scopes <csv>",
|
|
6541
|
+
"Comma-separated scope strings (e.g. `runtime:read,runtime:write`). Default: full install access."
|
|
6542
|
+
).option(
|
|
6543
|
+
"--expires-at <iso>",
|
|
6544
|
+
"ISO-8601 expiry timestamp (e.g. 2026-12-31T23:59:59Z). Default: never expires."
|
|
6545
|
+
).option(
|
|
6546
|
+
"--raw-only",
|
|
6547
|
+
"Print only the plaintext key on stdout; chatter goes to stderr. Useful for shell capture: `KEY=$(dashwise api-keys create prod --raw-only)`"
|
|
6548
|
+
).action(
|
|
6549
|
+
async (nameArg, cmdOpts) => {
|
|
6550
|
+
process.exitCode = await apiKeysCreateCommand(nameArg, {
|
|
6551
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6552
|
+
...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
|
|
6553
|
+
...cmdOpts.scopes !== void 0 ? { scopes: cmdOpts.scopes } : {},
|
|
6554
|
+
...cmdOpts.expiresAt !== void 0 ? { expiresAt: cmdOpts.expiresAt } : {},
|
|
6555
|
+
...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
|
|
6556
|
+
});
|
|
6557
|
+
}
|
|
6558
|
+
);
|
|
6559
|
+
apiKeysCmd.command("revoke [key-id]").description(
|
|
6560
|
+
"Soft-revoke an API key. Backend keeps the row for audit; further requests return 401."
|
|
6561
|
+
).option("--dir <path>", "Project root (default: current directory).").option("-y, --yes", "Skip the per-key confirmation message (for CI / scripts).").action(
|
|
6562
|
+
async (keyIdArg, cmdOpts) => {
|
|
6563
|
+
process.exitCode = await apiKeysRevokeCommand(keyIdArg, {
|
|
6564
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6565
|
+
...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
|
|
6566
|
+
});
|
|
6567
|
+
}
|
|
6568
|
+
);
|
|
6569
|
+
var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
|
|
6570
|
+
envCmd.command("export").description(
|
|
6571
|
+
"Emit the env vars a deployed module needs to authenticate against DashWise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
|
|
6572
|
+
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
6573
|
+
"--format <fmt>",
|
|
6574
|
+
"Output format: env | json | railway | vercel | fly (default: env)."
|
|
6575
|
+
).option(
|
|
6576
|
+
"--new-key <name>",
|
|
6577
|
+
"Mint a fresh API key with this name and inline the plaintext. (Mutually exclusive with --key.)"
|
|
6578
|
+
).option(
|
|
6579
|
+
"--key <dwk_value>",
|
|
6580
|
+
"Use a previously-minted API key (paste plaintext). (Mutually exclusive with --new-key.)"
|
|
6581
|
+
).action(
|
|
6582
|
+
async (cmdOpts) => {
|
|
6583
|
+
process.exitCode = await envExportCommand({
|
|
6584
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
6585
|
+
...cmdOpts.format !== void 0 ? { format: cmdOpts.format } : {},
|
|
6586
|
+
...cmdOpts.newKey !== void 0 ? { newKey: cmdOpts.newKey } : {},
|
|
6587
|
+
...cmdOpts.key !== void 0 ? { key: cmdOpts.key } : {}
|
|
6588
|
+
});
|
|
6589
|
+
}
|
|
6590
|
+
);
|
|
6008
6591
|
moduleCmd.command("publish").description("Pack the current directory + upload it as a new module version.").option("--dir <path>", "Project root (default: current directory).").option("--bump <direction>", "Semver bump: patch | minor | major.").option("--dry-run", "Validate + upload but do not persist the version row.").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option(
|
|
6009
6592
|
"--exclude <pattern...>",
|
|
6010
6593
|
"Extra exclude patterns (segment-matched) appended to the defaults."
|
|
@@ -6196,7 +6779,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
6196
6779
|
}
|
|
6197
6780
|
})();
|
|
6198
6781
|
function getVersion() {
|
|
6199
|
-
return "0.
|
|
6782
|
+
return "0.5.1";
|
|
6200
6783
|
}
|
|
6201
6784
|
function parseIntOption(value) {
|
|
6202
6785
|
const n = Number.parseInt(value, 10);
|