@dashai/cli 0.15.0 → 0.17.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 +549 -30
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -464,6 +464,19 @@ async function applyDraftManifest(moduleId, manifest, options = {}) {
|
|
|
464
464
|
options
|
|
465
465
|
);
|
|
466
466
|
}
|
|
467
|
+
async function getRuntimeLogs(installationId, opts = {}, options = {}) {
|
|
468
|
+
const params = new URLSearchParams();
|
|
469
|
+
if (opts.since !== void 0 && opts.since !== "") params.set("since", opts.since);
|
|
470
|
+
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
471
|
+
if (opts.tail) params.set("tail", "true");
|
|
472
|
+
const qs = params.toString();
|
|
473
|
+
return apiRequest(
|
|
474
|
+
"GET",
|
|
475
|
+
`/api/modules/installations/${installationId}/runtime-logs${qs ? `?${qs}` : ""}`,
|
|
476
|
+
void 0,
|
|
477
|
+
options
|
|
478
|
+
);
|
|
479
|
+
}
|
|
467
480
|
var ApiError;
|
|
468
481
|
var init_api_client = __esm({
|
|
469
482
|
"src/lib/api-client.ts"() {
|
|
@@ -840,6 +853,7 @@ async function devCommand(options) {
|
|
|
840
853
|
const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
|
|
841
854
|
let installationIdEnv;
|
|
842
855
|
let revokeDevSession;
|
|
856
|
+
let devHandoffEnv = {};
|
|
843
857
|
if (explicitInstallationId && explicitInstallationId !== "local") {
|
|
844
858
|
installationIdEnv = explicitInstallationId;
|
|
845
859
|
} else if (reuseDevSessionId) ; else {
|
|
@@ -862,10 +876,14 @@ async function devCommand(options) {
|
|
|
862
876
|
callback_origins: callbackOrigins
|
|
863
877
|
});
|
|
864
878
|
revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
|
|
879
|
+
devHandoffEnv = devSessionHandoffEnv(session);
|
|
865
880
|
info(` ${dim("dev session")} ${session.id}`);
|
|
866
881
|
info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
|
|
867
882
|
info(` ${dim("callbacks")} ${callbackOrigins.join(", ")}`);
|
|
868
883
|
info(` ${dim("expires")} ${session.expires_at}`);
|
|
884
|
+
info(
|
|
885
|
+
` ${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)"}`
|
|
886
|
+
);
|
|
869
887
|
} catch (err) {
|
|
870
888
|
if (err instanceof ApiError) {
|
|
871
889
|
fail(`Failed to register dev session: ${err.code} \u2014 ${err.message}`);
|
|
@@ -879,7 +897,8 @@ async function devCommand(options) {
|
|
|
879
897
|
...process.env,
|
|
880
898
|
DASHWISE_API_URL: apiUrl,
|
|
881
899
|
NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
|
|
882
|
-
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
|
|
900
|
+
...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {},
|
|
901
|
+
...devHandoffEnv
|
|
883
902
|
};
|
|
884
903
|
info(`Dev loop starting \u2014 Ctrl-C to stop.`);
|
|
885
904
|
info(` ${dim("api")} ${apiUrl}`);
|
|
@@ -1117,6 +1136,14 @@ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
|
|
|
1117
1136
|
success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
|
|
1118
1137
|
return upgraded;
|
|
1119
1138
|
}
|
|
1139
|
+
function devSessionHandoffEnv(session) {
|
|
1140
|
+
const secret = session.handoff_secret;
|
|
1141
|
+
if (typeof secret !== "string" || secret.length === 0) return {};
|
|
1142
|
+
return {
|
|
1143
|
+
DASHWISE_DEV_SESSION_ID: session.id,
|
|
1144
|
+
DASHWISE_DEV_HANDOFF_SECRET: secret
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1120
1147
|
function computeCallbackOrigins(port, extra) {
|
|
1121
1148
|
const def = `http://localhost:${port}`;
|
|
1122
1149
|
return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
|
|
@@ -1285,7 +1312,7 @@ function packageJson(opts) {
|
|
|
1285
1312
|
start: "next start"
|
|
1286
1313
|
},
|
|
1287
1314
|
dependencies: {
|
|
1288
|
-
"@dashai/sdk": "^2.
|
|
1315
|
+
"@dashai/sdk": "^2.3.0",
|
|
1289
1316
|
next: "^15.0.0",
|
|
1290
1317
|
react: "^19.0.0",
|
|
1291
1318
|
"react-dom": "^19.0.0"
|
|
@@ -1953,8 +1980,20 @@ function appApiAuthSignInTs() {
|
|
|
1953
1980
|
// 302 back to callback_url?code=<one-time>. The /api/auth/callback route
|
|
1954
1981
|
// exchanges that code for a session cookie.
|
|
1955
1982
|
//
|
|
1956
|
-
//
|
|
1957
|
-
//
|
|
1983
|
+
// ZERO-CLICK LOCAL DEV (CLI \u2265 0.16.0 + backend \u2265 2026-07-15): when
|
|
1984
|
+
// \`dashwise dev\` injected BOTH DASHWISE_DEV_SESSION_ID and
|
|
1985
|
+
// DASHWISE_DEV_HANDOFF_SECRET, this route first presents the handoff
|
|
1986
|
+
// secret to the backend SERVER-SIDE (POST /api/auth/dev/sso/authorize)
|
|
1987
|
+
// and 302s the browser straight to the returned callback URL \u2014 the user
|
|
1988
|
+
// NEVER sees a login page and no platform browser session is needed. On
|
|
1989
|
+
// ANY failure it logs one server-side warn line and falls back to the
|
|
1990
|
+
// browser flow above unchanged (that path still matters for signing in
|
|
1991
|
+
// as a DIFFERENT workspace member and for stale dev sessions). The
|
|
1992
|
+
// secret is server-env-only: never in a URL, response body, or the
|
|
1993
|
+
// client bundle.
|
|
1994
|
+
//
|
|
1995
|
+
// No dev-vs-prod branching for the browser flow \u2014 the single dwk_ key
|
|
1996
|
+
// drives both local dev and production.
|
|
1958
1997
|
import { NextResponse } from 'next/server';
|
|
1959
1998
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
1960
1999
|
|
|
@@ -1974,6 +2013,30 @@ export async function GET(request: Request) {
|
|
|
1974
2013
|
status: 500,
|
|
1975
2014
|
});
|
|
1976
2015
|
}
|
|
2016
|
+
|
|
2017
|
+
// \`getPublicOrigin\` reads \`X-Forwarded-Host\` when present so URLs
|
|
2018
|
+
// point at the public domain, not the internal Node origin behind
|
|
2019
|
+
// reverse proxies (Railway/Vercel/Fly).
|
|
2020
|
+
const moduleOrigin = getPublicOrigin(request);
|
|
2021
|
+
|
|
2022
|
+
// \u2500\u2500 Zero-click dev handoff (server-side; runs BEFORE the dwk_-key
|
|
2023
|
+
// guard \u2014 the authorize endpoint authenticates with the handoff
|
|
2024
|
+
// secret, not the key). Both env vars must be present: \`dashwise dev\`
|
|
2025
|
+
// injects them together, and only when the backend issued a secret.
|
|
2026
|
+
const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
|
|
2027
|
+
const devHandoffSecret = process.env.DASHWISE_DEV_HANDOFF_SECRET;
|
|
2028
|
+
if (devSessionId && devHandoffSecret) {
|
|
2029
|
+
const callbackUrl = await tryDevHandoff(
|
|
2030
|
+
apiBaseUrl,
|
|
2031
|
+
devSessionId,
|
|
2032
|
+
devHandoffSecret,
|
|
2033
|
+
returnTo,
|
|
2034
|
+
moduleOrigin,
|
|
2035
|
+
);
|
|
2036
|
+
if (callbackUrl) return NextResponse.redirect(callbackUrl);
|
|
2037
|
+
// null \u2192 warned server-side already; fall through to the browser flow.
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1977
2040
|
if (!apiKey) {
|
|
1978
2041
|
return new NextResponse(
|
|
1979
2042
|
'DASHWISE_API_KEY must be set in the environment. Run \`dashwise dev\` ' +
|
|
@@ -1983,12 +2046,9 @@ export async function GET(request: Request) {
|
|
|
1983
2046
|
}
|
|
1984
2047
|
|
|
1985
2048
|
// Absolute callback URL so the backend knows which host to bounce back
|
|
1986
|
-
// to.
|
|
1987
|
-
//
|
|
1988
|
-
//
|
|
1989
|
-
// /api/auth/callback by SDK convention; we tuck \`return_to\` onto it so
|
|
1990
|
-
// the callback route can resume the user's original destination.
|
|
1991
|
-
const moduleOrigin = getPublicOrigin(request);
|
|
2049
|
+
// to. The callback path is fixed at /api/auth/callback by SDK
|
|
2050
|
+
// convention; we tuck \`return_to\` onto it so the callback route can
|
|
2051
|
+
// resume the user's original destination.
|
|
1992
2052
|
const callback = new URL('/api/auth/callback', moduleOrigin);
|
|
1993
2053
|
callback.searchParams.set('return_to', returnTo);
|
|
1994
2054
|
|
|
@@ -2041,6 +2101,74 @@ export async function GET(request: Request) {
|
|
|
2041
2101
|
}
|
|
2042
2102
|
return NextResponse.redirect(body.redirect_url);
|
|
2043
2103
|
}
|
|
2104
|
+
|
|
2105
|
+
/**
|
|
2106
|
+
* Zero-click dev sign-in (CLI \u2265 0.16.0 + backend \u2265 2026-07-15).
|
|
2107
|
+
*
|
|
2108
|
+
* SERVER-SIDE ONLY: presents the dev session's one-time handoff secret
|
|
2109
|
+
* to \`POST /api/auth/dev/sso/authorize\`. On 200 the backend mints a
|
|
2110
|
+
* single-use 60s exchange code for the DEV'S OWN user and returns
|
|
2111
|
+
* \`{ callback_url }\` (= \`<origin>/api/auth/callback?code=\u2026&return_to=\u2026\`)
|
|
2112
|
+
* \u2014 the caller 302s the browser straight there; the normal callback
|
|
2113
|
+
* route exchanges the code exactly like the browser flow.
|
|
2114
|
+
*
|
|
2115
|
+
* Returns the callback URL, or \`null\` on ANY failure (404
|
|
2116
|
+
* DEV_SESSION_NOT_FOUND, 401 DEV_HANDOFF_INVALID, 400 INVALID_RETURN_TO,
|
|
2117
|
+
* 403 WORKSPACE_NOT_MEMBER, network) after logging a one-line warn \u2014
|
|
2118
|
+
* the caller then falls back to the browser SSO flow, which remains the
|
|
2119
|
+
* path for signing in as a DIFFERENT workspace member and for stale dev
|
|
2120
|
+
* sessions. The secret must never appear in a URL or response body.
|
|
2121
|
+
*/
|
|
2122
|
+
async function tryDevHandoff(
|
|
2123
|
+
apiBaseUrl: string,
|
|
2124
|
+
devSessionId: string,
|
|
2125
|
+
devHandoffSecret: string,
|
|
2126
|
+
returnTo: string,
|
|
2127
|
+
moduleOrigin: string,
|
|
2128
|
+
): Promise<string | null> {
|
|
2129
|
+
// Same root-relative-only rule as the callback's safeReturnTo \u2014 an
|
|
2130
|
+
// absolute/protocol-relative return_to must not steer the handoff.
|
|
2131
|
+
const path =
|
|
2132
|
+
returnTo.startsWith('/') && !returnTo.startsWith('//') ? returnTo : '/';
|
|
2133
|
+
// \`return_to\` is the user's ABSOLUTE destination on this module's own
|
|
2134
|
+
// (loopback) origin; the backend validates origin allowlist + loopback
|
|
2135
|
+
// and builds the callback URL from it.
|
|
2136
|
+
const destination = new URL(path, moduleOrigin);
|
|
2137
|
+
let res: Response;
|
|
2138
|
+
try {
|
|
2139
|
+
res = await fetch(\`\${apiBaseUrl}/api/auth/dev/sso/authorize\`, {
|
|
2140
|
+
method: 'POST',
|
|
2141
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
2142
|
+
body: JSON.stringify({
|
|
2143
|
+
dev_session_id: devSessionId,
|
|
2144
|
+
handoff_secret: devHandoffSecret,
|
|
2145
|
+
return_to: destination.toString(),
|
|
2146
|
+
}),
|
|
2147
|
+
});
|
|
2148
|
+
} catch (err) {
|
|
2149
|
+
console.warn(
|
|
2150
|
+
\`[auth] dev handoff failed (\${(err as Error).message}) \u2014 falling back to browser SSO\`,
|
|
2151
|
+
);
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
if (!res.ok) {
|
|
2155
|
+
const body = (await res.json().catch(() => null)) as { code?: string } | null;
|
|
2156
|
+
console.warn(
|
|
2157
|
+
\`[auth] dev handoff rejected (\${body?.code ?? \`HTTP \${res.status}\`}) \u2014 falling back to browser SSO\`,
|
|
2158
|
+
);
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
const body = (await res.json().catch(() => null)) as
|
|
2162
|
+
| { callback_url?: string }
|
|
2163
|
+
| null;
|
|
2164
|
+
if (!body?.callback_url) {
|
|
2165
|
+
console.warn(
|
|
2166
|
+
'[auth] dev handoff response missing callback_url \u2014 falling back to browser SSO',
|
|
2167
|
+
);
|
|
2168
|
+
return null;
|
|
2169
|
+
}
|
|
2170
|
+
return body.callback_url;
|
|
2171
|
+
}
|
|
2044
2172
|
`;
|
|
2045
2173
|
}
|
|
2046
2174
|
function appApiAuthCallbackTs() {
|
|
@@ -2049,7 +2177,14 @@ function appApiAuthCallbackTs() {
|
|
|
2049
2177
|
// SSO flow failed. Two paths:
|
|
2050
2178
|
//
|
|
2051
2179
|
// 1. Success path: exchange \`?code\` for a session cookie via
|
|
2052
|
-
// \`exchangeCode()\`, then redirect to \`return_to\`.
|
|
2180
|
+
// \`exchangeCode()\`, then redirect to \`return_to\`. SDK \u2265 2.3.0:
|
|
2181
|
+
// the result's \`cookieWritten\` flag surfaces a cookie store that
|
|
2182
|
+
// rejected the write \u2014 without it the exchange "succeeds", the
|
|
2183
|
+
// browser has no session, and the user silently bounces back to
|
|
2184
|
+
// sign-in. We render /sign-in-error?code=cookie_write_failed
|
|
2185
|
+
// instead. \`requestUrl\` lets the SDK fall back to the actual
|
|
2186
|
+
// request protocol for the cookie's Secure attribute when no
|
|
2187
|
+
// x-forwarded-proto header is present (plain-http \`next start\`).
|
|
2053
2188
|
// 2. Error path: SSO returned \`?error=\` \u2014 redirect to the polished
|
|
2054
2189
|
// sign-in-error page so the user sees a real error UI instead of
|
|
2055
2190
|
// a raw 4xx page. The error code maps to known SSO error codes
|
|
@@ -2086,7 +2221,17 @@ export async function GET(request: Request) {
|
|
|
2086
2221
|
return NextResponse.redirect(target);
|
|
2087
2222
|
}
|
|
2088
2223
|
try {
|
|
2089
|
-
|
|
2224
|
+
// Sets the dashwise.session cookie. \`requestUrl\` = the Secure-attr
|
|
2225
|
+
// protocol fallback when no x-forwarded-proto header is present.
|
|
2226
|
+
const result = await exchangeCode(code, { requestUrl: request.url });
|
|
2227
|
+
if (!result.cookieWritten) {
|
|
2228
|
+
// The exchange succeeded but the cookie store rejected the write \u2014
|
|
2229
|
+
// the browser has NO session. Surface it instead of redirecting
|
|
2230
|
+
// into an unauthenticated loop (SDK \u2265 2.3.0).
|
|
2231
|
+
const target = new URL('/sign-in-error', publicOrigin);
|
|
2232
|
+
target.searchParams.set('code', 'cookie_write_failed');
|
|
2233
|
+
return NextResponse.redirect(target);
|
|
2234
|
+
}
|
|
2090
2235
|
} catch (err) {
|
|
2091
2236
|
const target = new URL('/sign-in-error', publicOrigin);
|
|
2092
2237
|
target.searchParams.set('code', 'exchange_failed');
|
|
@@ -2123,7 +2268,12 @@ import { signOut } from '@dashai/sdk/auth/server';
|
|
|
2123
2268
|
import { getPublicOrigin } from '@/lib/request-origin';
|
|
2124
2269
|
|
|
2125
2270
|
export async function POST(request: Request) {
|
|
2126
|
-
|
|
2271
|
+
// Pass requestUrl so the cookie-CLEAR resolves its Secure attribute the
|
|
2272
|
+
// SAME way the sign-in SET did (exchangeCode also gets request.url) \u2014
|
|
2273
|
+
// otherwise a plain-http localhost dev server sets a non-Secure cookie
|
|
2274
|
+
// but tries to clear a Secure one, which the browser ignores, leaving a
|
|
2275
|
+
// stale (backend-revoked, but client-present) cookie after sign-out.
|
|
2276
|
+
await signOut({ requestUrl: request.url });
|
|
2127
2277
|
// Build the redirect against the public origin so we don't land
|
|
2128
2278
|
// the user on \`http://localhost:<PORT>/\` when running behind a
|
|
2129
2279
|
// reverse proxy (Railway/Vercel/Fly).
|
|
@@ -2210,7 +2360,7 @@ function packageJsonCustom(opts) {
|
|
|
2210
2360
|
start: "next start"
|
|
2211
2361
|
},
|
|
2212
2362
|
dependencies: {
|
|
2213
|
-
"@dashai/sdk": "^2.
|
|
2363
|
+
"@dashai/sdk": "^2.3.0",
|
|
2214
2364
|
next: "^15.0.0",
|
|
2215
2365
|
react: "^19.0.0",
|
|
2216
2366
|
"react-dom": "^19.0.0",
|
|
@@ -2711,6 +2861,8 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
2711
2861
|
'The callback URL was missing the expected exchange code. This usually means the SSO flow was interrupted.',
|
|
2712
2862
|
exchange_failed:
|
|
2713
2863
|
"The exchange code couldn't be redeemed for a session. The code may have expired (they're single-use and short-lived) \u2014 try signing in again.",
|
|
2864
|
+
cookie_write_failed:
|
|
2865
|
+
'Sign-in completed, but the session cookie could not be written, so the session would have been lost. This means exchangeCode ran outside a Route Handler / Server Action context (Next can only set cookies there) or next/headers was unavailable \u2014 verify /api/auth/callback is a standard route handler and check the module server logs.',
|
|
2714
2866
|
};
|
|
2715
2867
|
|
|
2716
2868
|
interface SignInErrorPageProps {
|
|
@@ -3517,6 +3669,14 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
3517
3669
|
`Not authorized to create a module in that workspace. Confirm you're a member at the DashWise dashboard.`
|
|
3518
3670
|
);
|
|
3519
3671
|
return;
|
|
3672
|
+
case "DEV_INSTALL_ALREADY_EXISTS": {
|
|
3673
|
+
const cached = readModuleEntry(slug);
|
|
3674
|
+
warn(
|
|
3675
|
+
`Module ${code(slug)} already has a live dev install${cached?.installationId ? ` (cached id=${cached.installationId})` : ""} \u2014 only ONE dev install per module can exist.
|
|
3676
|
+
Run ${code("dashwise module reset-dev-install")} for a clean slate (keeps ALL local files), or ${code("dashwise module destroy-dev-install")} then re-run init.`
|
|
3677
|
+
);
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3520
3680
|
case "SLUG_ALREADY_EXISTS": {
|
|
3521
3681
|
const cached = readModuleEntry(slug);
|
|
3522
3682
|
if (cached) {
|
|
@@ -3526,9 +3686,9 @@ function surfaceProvisionApiError(err, slug) {
|
|
|
3526
3686
|
);
|
|
3527
3687
|
} else {
|
|
3528
3688
|
warn(
|
|
3529
|
-
`A module with slug ${code(slug)} already exists
|
|
3530
|
-
-
|
|
3531
|
-
-
|
|
3689
|
+
`A module with slug ${code(slug)} already exists.
|
|
3690
|
+
- If it belongs to ANOTHER workspace: module slugs are globally unique \u2014 pick a NEW slug (duplicate the code if needed).
|
|
3691
|
+
- If it's YOUR OWN published module: backends \u2265 2026-07-15 re-provision a dev install from this exact command (${code(`dashwise init ${slug} --workspace <ws>`)} \u2014 the dev install coexists with the production install), so this 409 means the backend predates that. Older-backend fallback (production repair): ${code("dashwise api-keys create local-dev --install <id> --full-access")} + ${code("dashwise origins add http://localhost:3000 --install <id>")} (id via ${code(`dashwise module status ${slug}`)}), then run bare ${code("next dev")} \u2014 and CLEAN UP after (revoke the key, remove the origin).`
|
|
3532
3692
|
);
|
|
3533
3693
|
}
|
|
3534
3694
|
return;
|
|
@@ -4177,9 +4337,29 @@ function parseFieldSpec(spec) {
|
|
|
4177
4337
|
`The ${quote("unique")} flag is not supported on field specs \u2014 uniqueness is a TABLE-level index. Declare it in module.json as ${quote(`owns.tables[].indexes: [{ "fields": ["${slug}"], "unique": true }]`)} and apply it with ${quote("dashwise schema push")}.`
|
|
4178
4338
|
);
|
|
4179
4339
|
}
|
|
4340
|
+
if (DECIMALS_FLAG_REGEX.test(flag)) {
|
|
4341
|
+
if (type !== "number") {
|
|
4342
|
+
throw new FieldSpecError(
|
|
4343
|
+
`Decimal places (${quote(flag)}) are only valid on ${quote("number")} fields, not ${quote(type)}. Field spec ${quote(spec)} sets a scale on a non-number type.`
|
|
4344
|
+
);
|
|
4345
|
+
}
|
|
4346
|
+
if (result.decimal_places !== void 0) {
|
|
4347
|
+
throw new FieldSpecError(
|
|
4348
|
+
`Decimal places specified twice in field spec ${quote(spec)}.`
|
|
4349
|
+
);
|
|
4350
|
+
}
|
|
4351
|
+
const places = Number.parseInt(flag, 10);
|
|
4352
|
+
if (places < 0 || places > MAX_DECIMAL_PLACES) {
|
|
4353
|
+
throw new FieldSpecError(
|
|
4354
|
+
`Decimal places must be between 0 and ${MAX_DECIMAL_PLACES} (got ${quote(flag)}) in field spec ${quote(spec)}.`
|
|
4355
|
+
);
|
|
4356
|
+
}
|
|
4357
|
+
result.decimal_places = places;
|
|
4358
|
+
continue;
|
|
4359
|
+
}
|
|
4180
4360
|
if (!KNOWN_FLAGS.has(flag)) {
|
|
4181
4361
|
throw new FieldSpecError(
|
|
4182
|
-
`Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}. For options/target/default, use ${quote("--field-json")} instead.`
|
|
4362
|
+
`Unknown flag ${quote(flag)} in field spec ${quote(spec)}. Known flags: ${[...KNOWN_FLAGS].join(", ")}${type === "number" ? ` (plus a bare 0\u2013${MAX_DECIMAL_PLACES} integer for decimal places, e.g. ${quote(`${slug}:number:2`)})` : ""}. For options/target/default, use ${quote("--field-json")} instead.`
|
|
4183
4363
|
);
|
|
4184
4364
|
}
|
|
4185
4365
|
if (flag === "required") result.required = true;
|
|
@@ -4230,7 +4410,7 @@ function parseFieldJson(json) {
|
|
|
4230
4410
|
function quote(s) {
|
|
4231
4411
|
return `\`${s}\``;
|
|
4232
4412
|
}
|
|
4233
|
-
var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, FieldSpecError;
|
|
4413
|
+
var FIELD_TYPES, SLUG_REGEX, KNOWN_FLAGS, DECIMALS_FLAG_REGEX, MAX_DECIMAL_PLACES, FieldSpecError;
|
|
4234
4414
|
var init_field_spec = __esm({
|
|
4235
4415
|
"src/lib/field-spec.ts"() {
|
|
4236
4416
|
FIELD_TYPES = [
|
|
@@ -4253,6 +4433,8 @@ var init_field_spec = __esm({
|
|
|
4253
4433
|
];
|
|
4254
4434
|
SLUG_REGEX = /^[a-z][a-z0-9_]*[a-z0-9]$/;
|
|
4255
4435
|
KNOWN_FLAGS = /* @__PURE__ */ new Set(["required"]);
|
|
4436
|
+
DECIMALS_FLAG_REGEX = /^\d+$/;
|
|
4437
|
+
MAX_DECIMAL_PLACES = 10;
|
|
4256
4438
|
FieldSpecError = class extends Error {
|
|
4257
4439
|
constructor(message) {
|
|
4258
4440
|
super(message);
|
|
@@ -6275,13 +6457,14 @@ async function moduleInstallCommand(slugArg, options) {
|
|
|
6275
6457
|
return 0;
|
|
6276
6458
|
}
|
|
6277
6459
|
function publishedModuleLocalDevGuidance(slug) {
|
|
6278
|
-
return `
|
|
6460
|
+
return `Re-provision local dev any time with ${code(`dashwise init ${slug} --workspace <ws>`)} (or ${code("dashwise module reset-dev-install")}) \u2014 requires backend \u2265 2026-07-15; the fresh dev install COEXISTS with the production install.
|
|
6461
|
+
Older backends 409 SLUG_ALREADY_EXISTS on that path (no re-provision anywhere). Fallback for them (production repair):
|
|
6279
6462
|
- Develop against the production install with a scoped key: ${code(`dashwise api-keys create local-dev --install <id> --full-access`)} + ${code(`dashwise origins add http://localhost:3000 --install <id>`)} (id via ${code(`dashwise module status ${slug}`)}), then run bare ${code("next dev")} \u2014 and CLEAN UP after (revoke the key, remove the origin).
|
|
6280
6463
|
- Or duplicate the code under a NEW slug for greenfield iteration.`;
|
|
6281
6464
|
}
|
|
6282
6465
|
function printDevInstallAftermath(slug, workspaceSlug) {
|
|
6283
6466
|
warn(
|
|
6284
|
-
`Local dev for ${code(slug)} is
|
|
6467
|
+
`Local dev for ${code(slug)} is DISCONNECTED (the dev install in ${code(workspaceSlug)} and its ${code("dwk_")} key were destroyed).
|
|
6285
6468
|
${publishedModuleLocalDevGuidance(slug)}`
|
|
6286
6469
|
);
|
|
6287
6470
|
}
|
|
@@ -6305,9 +6488,9 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
6305
6488
|
return 1;
|
|
6306
6489
|
}
|
|
6307
6490
|
const explanation = `Workspace ${code(workspaceSlug)} already has the DEV install of ${code(slug)} (kind=local, id=${existing.id}) created by ${code("dashwise init")}.
|
|
6308
|
-
|
|
6491
|
+
Install ${code("kind")} is immutable: a plain install could land on that row's upgrade path \u2014 the runtime would provision, but it would STAY a dev install, hidden from the app sidebar.
|
|
6309
6492
|
Options:
|
|
6310
|
-
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done.
|
|
6493
|
+
- ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Local dev can be re-provisioned afterwards with ${code(`dashwise init ${slug} --workspace <ws>`)} on backends \u2265 2026-07-15 (dev + production installs coexist); on OLDER backends this is one-way \u2014 no re-provision anywhere once published (interim: a scoped key on the production install via ${code("api-keys create --install <id> --full-access")}, or duplicate the code under a NEW slug).
|
|
6311
6494
|
- ${code("--workspace <other>")} \u2014 deploy to a separate test/client workspace and keep this dev install. Recommended for ongoing development.`;
|
|
6312
6495
|
if (options.replaceDevInstall) {
|
|
6313
6496
|
info(explanation);
|
|
@@ -6321,7 +6504,7 @@ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options
|
|
|
6321
6504
|
{
|
|
6322
6505
|
value: "replace",
|
|
6323
6506
|
label: "Replace \u2014 destroy the dev install, then production-install",
|
|
6324
|
-
hint: "local
|
|
6507
|
+
hint: "revokes local-dev credentials; re-provision later via `dashwise init` (backend \u2265 2026-07-15 \u2014 older backends: scoped-key repair only)"
|
|
6325
6508
|
},
|
|
6326
6509
|
{ value: "cancel", label: "Cancel \u2014 I will deploy to another workspace" }
|
|
6327
6510
|
],
|
|
@@ -7093,13 +7276,13 @@ async function moduleUninstallCommand(slugArg, options) {
|
|
|
7093
7276
|
}
|
|
7094
7277
|
info(
|
|
7095
7278
|
`Uninstalling ${code(mod.slug)} (install ${code(`#${row.id}`)}, kind=${row.kind ?? "production"}${row.module_version?.version ? `, v${row.module_version.version}` : ""}) from workspace ${code(workspace2.slug)}.
|
|
7096
|
-
The app is soft-deleted
|
|
7097
|
-
|
|
7279
|
+
The app is soft-deleted and appears in your workspace trash \u2014 a workspace admin can self-serve restore its DATA (~30 days). Reviving a running module within ~7 days is a platform-operator action.
|
|
7280
|
+
A REINSTALL now KEEPS the same public URL${row.public_url ? ` ${code(row.public_url)}` : ""}, so deep links survive. (On backends older than 2026-07-15: restore is operator-only and a reinstall mints a new URL.)`
|
|
7098
7281
|
);
|
|
7099
7282
|
const answer = await select({
|
|
7100
7283
|
message: `Uninstall ${code(mod.slug)} from ${code(workspace2.slug)}?`,
|
|
7101
7284
|
options: [
|
|
7102
|
-
{ value: "uninstall", label: "Uninstall \u2014 soft-delete the app (
|
|
7285
|
+
{ value: "uninstall", label: "Uninstall \u2014 soft-delete the app (data restorable from workspace trash ~30 days)" },
|
|
7103
7286
|
{ value: "cancel", label: "Cancel \u2014 nothing is changed" }
|
|
7104
7287
|
],
|
|
7105
7288
|
initialValue: "cancel"
|
|
@@ -7131,8 +7314,8 @@ async function moduleUninstallCommand(slugArg, options) {
|
|
|
7131
7314
|
`Uninstalled ${code(mod.slug)} (install ${code(`#${row.id}`)}) from workspace ${code(workspace2.slug)}.`
|
|
7132
7315
|
);
|
|
7133
7316
|
info(
|
|
7134
|
-
`The app was soft-deleted. If this was a mistake:
|
|
7135
|
-
Reinstalling
|
|
7317
|
+
`The app was soft-deleted. If this was a mistake: restore its DATA yourself from your workspace trash (~30 days); to bring the running module back within ~7 days, contact a platform operator (support/HQ).
|
|
7318
|
+
Reinstalling (${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}) now KEEPS the same public URL. (Older backends, < 2026-07-15: restore is operator-only and reinstall mints a new URL.)`
|
|
7136
7319
|
);
|
|
7137
7320
|
if (options.parkConsumers) {
|
|
7138
7321
|
info(
|
|
@@ -10341,6 +10524,319 @@ function mapAndPrintError(err, json) {
|
|
|
10341
10524
|
return 1;
|
|
10342
10525
|
}
|
|
10343
10526
|
|
|
10527
|
+
// src/commands/module/logs.ts
|
|
10528
|
+
init_api_client();
|
|
10529
|
+
init_config();
|
|
10530
|
+
init_manifest_io();
|
|
10531
|
+
init_output();
|
|
10532
|
+
var POLL_INTERVAL_MS3 = 2e3;
|
|
10533
|
+
var DURATION_UNIT_MS = {
|
|
10534
|
+
s: 1e3,
|
|
10535
|
+
m: 6e4,
|
|
10536
|
+
h: 36e5,
|
|
10537
|
+
d: 864e5
|
|
10538
|
+
};
|
|
10539
|
+
function resolveSince(raw) {
|
|
10540
|
+
if (raw === void 0) return void 0;
|
|
10541
|
+
const trimmed = raw.trim();
|
|
10542
|
+
if (trimmed === "") return void 0;
|
|
10543
|
+
const m = /^(\d+)\s*(s|m|h|d)$/i.exec(trimmed);
|
|
10544
|
+
if (m) {
|
|
10545
|
+
const n = Number.parseInt(m[1], 10);
|
|
10546
|
+
const unitMs = DURATION_UNIT_MS[m[2].toLowerCase()];
|
|
10547
|
+
return new Date(Date.now() - n * unitMs).toISOString();
|
|
10548
|
+
}
|
|
10549
|
+
return trimmed;
|
|
10550
|
+
}
|
|
10551
|
+
async function moduleLogsCommand(slugArg, options) {
|
|
10552
|
+
if (options.follow && options.json) {
|
|
10553
|
+
fail("`--follow` and `--json` are mutually exclusive.");
|
|
10554
|
+
return 1;
|
|
10555
|
+
}
|
|
10556
|
+
if (!isLoggedIn()) {
|
|
10557
|
+
fail(`Not logged in. Run ${code("dashwise login")} first.`);
|
|
10558
|
+
return 1;
|
|
10559
|
+
}
|
|
10560
|
+
const cfg = readConfig();
|
|
10561
|
+
if (!cfg) {
|
|
10562
|
+
fail(`CLI config missing. Run ${code("dashwise login")}.`);
|
|
10563
|
+
return 1;
|
|
10564
|
+
}
|
|
10565
|
+
const auth = {
|
|
10566
|
+
baseUrl: resolveApiUrl(),
|
|
10567
|
+
auth: { token: cfg.token }
|
|
10568
|
+
};
|
|
10569
|
+
const since = resolveSince(options.since);
|
|
10570
|
+
let installationId;
|
|
10571
|
+
let label;
|
|
10572
|
+
if (options.install !== void 0) {
|
|
10573
|
+
if (!Number.isInteger(options.install) || options.install <= 0) {
|
|
10574
|
+
fail(
|
|
10575
|
+
`--install expects a positive integer installation id (got "${options.install}").`
|
|
10576
|
+
);
|
|
10577
|
+
return 1;
|
|
10578
|
+
}
|
|
10579
|
+
installationId = options.install;
|
|
10580
|
+
label = `install #${installationId}`;
|
|
10581
|
+
} else {
|
|
10582
|
+
const resolved = await resolveInstallationFromSlug(slugArg, options, cfg, auth);
|
|
10583
|
+
if (typeof resolved === "number") return resolved;
|
|
10584
|
+
installationId = resolved.installationId;
|
|
10585
|
+
label = resolved.label;
|
|
10586
|
+
}
|
|
10587
|
+
if (options.follow) {
|
|
10588
|
+
return runFollow(installationId, label, { since, limit: options.limit }, auth);
|
|
10589
|
+
}
|
|
10590
|
+
return runOnce3(installationId, label, options, since, auth);
|
|
10591
|
+
}
|
|
10592
|
+
async function resolveInstallationFromSlug(slugArg, options, cfg, auth) {
|
|
10593
|
+
let slug = slugArg;
|
|
10594
|
+
if (!slug) {
|
|
10595
|
+
try {
|
|
10596
|
+
const fromManifest = readManifest(options.dir)?.module?.slug;
|
|
10597
|
+
if (typeof fromManifest === "string" && fromManifest.length > 0) {
|
|
10598
|
+
slug = fromManifest;
|
|
10599
|
+
}
|
|
10600
|
+
} catch {
|
|
10601
|
+
}
|
|
10602
|
+
}
|
|
10603
|
+
if (!slug) {
|
|
10604
|
+
fail(
|
|
10605
|
+
`No module slug \u2014 pass one (${code("dashwise module logs <slug>")}), run from a directory with a valid module.json, or target an install directly with ${code("--install <id>")}.`
|
|
10606
|
+
);
|
|
10607
|
+
return 1;
|
|
10608
|
+
}
|
|
10609
|
+
let modId;
|
|
10610
|
+
let modSlug;
|
|
10611
|
+
try {
|
|
10612
|
+
const mod = await getModuleBySlug(slug, auth);
|
|
10613
|
+
modId = mod.id;
|
|
10614
|
+
modSlug = mod.slug;
|
|
10615
|
+
} catch (err) {
|
|
10616
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
10617
|
+
fail(`Module ${code(slug)} not found on the backend.`);
|
|
10618
|
+
return 1;
|
|
10619
|
+
}
|
|
10620
|
+
fail(`Could not resolve module ${code(slug)}: ${err.message}`);
|
|
10621
|
+
return 1;
|
|
10622
|
+
}
|
|
10623
|
+
let workspace2;
|
|
10624
|
+
try {
|
|
10625
|
+
workspace2 = await resolveWorkspaceWithFallback({
|
|
10626
|
+
userFlag: options.workspace,
|
|
10627
|
+
cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
|
|
10628
|
+
configDefault: cfg.workspaceSlug,
|
|
10629
|
+
apiUrl: resolveApiUrl(),
|
|
10630
|
+
apiToken: cfg.token
|
|
10631
|
+
});
|
|
10632
|
+
} catch (err) {
|
|
10633
|
+
fail(`Workspace selection failed: ${err.message}`);
|
|
10634
|
+
return 1;
|
|
10635
|
+
}
|
|
10636
|
+
let row;
|
|
10637
|
+
try {
|
|
10638
|
+
const installs = await listWorkspaceInstallations(workspace2.id, auth);
|
|
10639
|
+
const matches = installs.filter((r) => r.module_id === modId);
|
|
10640
|
+
row = matches.find((r) => !isDevInstallKind(r.kind) && isRealRuntimeUrl(r.runtime_url)) ?? matches.find((r) => !isDevInstallKind(r.kind)) ?? matches[0];
|
|
10641
|
+
} catch (err) {
|
|
10642
|
+
fail(`Could not read workspace installations: ${err.message}`);
|
|
10643
|
+
return 1;
|
|
10644
|
+
}
|
|
10645
|
+
if (!row) {
|
|
10646
|
+
info(
|
|
10647
|
+
`${code(modSlug)} is not installed in workspace ${code(workspace2.slug)} \u2014 nothing to read logs from. Deploy it with ${code(`dashwise module install ${modSlug} --workspace ${workspace2.slug}`)}.`
|
|
10648
|
+
);
|
|
10649
|
+
return 0;
|
|
10650
|
+
}
|
|
10651
|
+
return {
|
|
10652
|
+
installationId: row.id,
|
|
10653
|
+
label: `${modSlug} in ${workspace2.slug} (install #${row.id})`
|
|
10654
|
+
};
|
|
10655
|
+
}
|
|
10656
|
+
async function runOnce3(installationId, label, options, since, auth) {
|
|
10657
|
+
let body;
|
|
10658
|
+
try {
|
|
10659
|
+
body = await getRuntimeLogs(
|
|
10660
|
+
installationId,
|
|
10661
|
+
{
|
|
10662
|
+
...since !== void 0 && !options.tail ? { since } : {},
|
|
10663
|
+
...options.limit !== void 0 ? { limit: options.limit } : {},
|
|
10664
|
+
...options.tail ? { tail: true } : {}
|
|
10665
|
+
},
|
|
10666
|
+
auth
|
|
10667
|
+
);
|
|
10668
|
+
} catch (err) {
|
|
10669
|
+
return mapLogsError(err, options.json);
|
|
10670
|
+
}
|
|
10671
|
+
if (options.json) {
|
|
10672
|
+
out(JSON.stringify(body));
|
|
10673
|
+
return 0;
|
|
10674
|
+
}
|
|
10675
|
+
if (body.entries.length === 0) {
|
|
10676
|
+
info(`No runtime log entries for ${label} in the requested window.`);
|
|
10677
|
+
return 0;
|
|
10678
|
+
}
|
|
10679
|
+
for (const entry of body.entries) out(formatRow2(entry));
|
|
10680
|
+
out("");
|
|
10681
|
+
info(
|
|
10682
|
+
`${body.entries.length} ${body.entries.length === 1 ? "line" : "lines"} (${label}).`
|
|
10683
|
+
);
|
|
10684
|
+
return 0;
|
|
10685
|
+
}
|
|
10686
|
+
async function runFollow(installationId, label, opts, auth) {
|
|
10687
|
+
info(`Following runtime logs for ${label} (every ${POLL_INTERVAL_MS3 / 1e3}s). Ctrl-C to stop.`);
|
|
10688
|
+
out("");
|
|
10689
|
+
let stopped = false;
|
|
10690
|
+
const onSigint = () => {
|
|
10691
|
+
stopped = true;
|
|
10692
|
+
};
|
|
10693
|
+
process.on("SIGINT", onSigint);
|
|
10694
|
+
let lastTs;
|
|
10695
|
+
let seenAtLastTs = /* @__PURE__ */ new Set();
|
|
10696
|
+
let firstPoll = true;
|
|
10697
|
+
try {
|
|
10698
|
+
while (!stopped) {
|
|
10699
|
+
const query = {};
|
|
10700
|
+
if (opts.limit !== void 0) query.limit = opts.limit;
|
|
10701
|
+
if (firstPoll) {
|
|
10702
|
+
if (opts.since !== void 0) query.since = opts.since;
|
|
10703
|
+
else query.tail = true;
|
|
10704
|
+
} else if (lastTs !== void 0) {
|
|
10705
|
+
query.since = new Date(lastTs).toISOString();
|
|
10706
|
+
}
|
|
10707
|
+
let body;
|
|
10708
|
+
try {
|
|
10709
|
+
body = await getRuntimeLogs(installationId, query, auth);
|
|
10710
|
+
} catch (err) {
|
|
10711
|
+
if (isFatalFollowError(err)) {
|
|
10712
|
+
return mapLogsError(err, false);
|
|
10713
|
+
}
|
|
10714
|
+
info(dim(`(follow: transient error \u2014 ${err.message})`));
|
|
10715
|
+
await sleep4(POLL_INTERVAL_MS3, () => stopped);
|
|
10716
|
+
firstPoll = false;
|
|
10717
|
+
continue;
|
|
10718
|
+
}
|
|
10719
|
+
const fresh = [];
|
|
10720
|
+
for (const entry of body.entries) {
|
|
10721
|
+
if (lastTs !== void 0 && entry.ts < lastTs) continue;
|
|
10722
|
+
if (lastTs !== void 0 && entry.ts === lastTs && seenAtLastTs.has(entryKey(entry))) {
|
|
10723
|
+
continue;
|
|
10724
|
+
}
|
|
10725
|
+
fresh.push(entry);
|
|
10726
|
+
}
|
|
10727
|
+
fresh.sort((a, b) => a.ts - b.ts);
|
|
10728
|
+
for (const entry of fresh) out(formatRow2(entry));
|
|
10729
|
+
if (fresh.length > 0) {
|
|
10730
|
+
const newMax = fresh[fresh.length - 1].ts;
|
|
10731
|
+
if (newMax !== lastTs) seenAtLastTs = /* @__PURE__ */ new Set();
|
|
10732
|
+
lastTs = newMax;
|
|
10733
|
+
for (const entry of fresh) {
|
|
10734
|
+
if (entry.ts === lastTs) seenAtLastTs.add(entryKey(entry));
|
|
10735
|
+
}
|
|
10736
|
+
}
|
|
10737
|
+
firstPoll = false;
|
|
10738
|
+
await sleep4(POLL_INTERVAL_MS3, () => stopped);
|
|
10739
|
+
}
|
|
10740
|
+
} finally {
|
|
10741
|
+
process.off("SIGINT", onSigint);
|
|
10742
|
+
}
|
|
10743
|
+
out("");
|
|
10744
|
+
return 0;
|
|
10745
|
+
}
|
|
10746
|
+
function isFatalFollowError(err) {
|
|
10747
|
+
if (!(err instanceof ApiError)) return false;
|
|
10748
|
+
if (err.status === 401 || err.status === 403 || err.status === 404) return true;
|
|
10749
|
+
return err.code === "INSTALLATION_NOT_FOUND" || err.code === "RUNTIME_NOT_PROVISIONED" || err.code === "INVALID_SINCE";
|
|
10750
|
+
}
|
|
10751
|
+
function formatTs(ts) {
|
|
10752
|
+
if (!Number.isFinite(ts)) return "--:--:--";
|
|
10753
|
+
return new Date(ts).toISOString().slice(11, 19);
|
|
10754
|
+
}
|
|
10755
|
+
function colorLevel(level) {
|
|
10756
|
+
const padded = level.toUpperCase().padEnd(5);
|
|
10757
|
+
switch (level) {
|
|
10758
|
+
case "error":
|
|
10759
|
+
return pc.red(padded);
|
|
10760
|
+
case "warn":
|
|
10761
|
+
return pc.yellow(padded);
|
|
10762
|
+
case "info":
|
|
10763
|
+
return pc.green(padded);
|
|
10764
|
+
case "debug":
|
|
10765
|
+
default:
|
|
10766
|
+
return dim(padded);
|
|
10767
|
+
}
|
|
10768
|
+
}
|
|
10769
|
+
function formatRow2(entry) {
|
|
10770
|
+
const time = dim(formatTs(entry.ts));
|
|
10771
|
+
const level = colorLevel(entry.level);
|
|
10772
|
+
const reqId = entry.request_id ? ` ${dim(`[${entry.request_id}]`)}` : "";
|
|
10773
|
+
return `${time} ${level} ${entry.message}${reqId}`;
|
|
10774
|
+
}
|
|
10775
|
+
function entryKey(entry) {
|
|
10776
|
+
return `${entry.ts}\0${entry.request_id ?? ""}\0${entry.message}`;
|
|
10777
|
+
}
|
|
10778
|
+
function mapLogsError(err, json) {
|
|
10779
|
+
if (err instanceof ApiError) {
|
|
10780
|
+
const emit = (code_, message2) => {
|
|
10781
|
+
if (json) out(JSON.stringify({ ok: false, error: { code: code_, message: message2 } }));
|
|
10782
|
+
else fail(message2);
|
|
10783
|
+
};
|
|
10784
|
+
if (err.status === 401 || err.code === "UNAUTHENTICATED") {
|
|
10785
|
+
emit("NOT_LOGGED_IN", `Not logged in. Run ${code("dashwise login")} first.`);
|
|
10786
|
+
return 1;
|
|
10787
|
+
}
|
|
10788
|
+
if (err.status === 403) {
|
|
10789
|
+
emit(
|
|
10790
|
+
"FORBIDDEN",
|
|
10791
|
+
`Reading runtime logs requires workspace-admin on this install's workspace.`
|
|
10792
|
+
);
|
|
10793
|
+
return 1;
|
|
10794
|
+
}
|
|
10795
|
+
if (err.status === 404 || err.code === "INSTALLATION_NOT_FOUND") {
|
|
10796
|
+
emit(
|
|
10797
|
+
"INSTALLATION_NOT_FOUND",
|
|
10798
|
+
`Installation not found. Check the id (${code("dashwise module status <slug>")}) \u2014 it may have been uninstalled.`
|
|
10799
|
+
);
|
|
10800
|
+
return 1;
|
|
10801
|
+
}
|
|
10802
|
+
if (err.code === "RUNTIME_NOT_PROVISIONED") {
|
|
10803
|
+
emit(
|
|
10804
|
+
"RUNTIME_NOT_PROVISIONED",
|
|
10805
|
+
`This install has no running runtime yet \u2014 deploy it first (${code("dashwise module install <slug>")}). The ${code("kind=local")} dev install never provisions a machine runtime, so it has no runtime logs.`
|
|
10806
|
+
);
|
|
10807
|
+
return 1;
|
|
10808
|
+
}
|
|
10809
|
+
if (err.code === "INVALID_SINCE") {
|
|
10810
|
+
emit(
|
|
10811
|
+
"INVALID_SINCE",
|
|
10812
|
+
`${code("--since")} must be an ISO-8601 timestamp (e.g. ${code("2026-07-15T09:00:00Z")}) or a duration like ${code("15m")} / ${code("2h")} / ${code("2d")}.`
|
|
10813
|
+
);
|
|
10814
|
+
return 1;
|
|
10815
|
+
}
|
|
10816
|
+
if (err.code.startsWith("RUNTIME_")) {
|
|
10817
|
+
emit(
|
|
10818
|
+
err.code,
|
|
10819
|
+
`The runtime host rejected the log read (${err.code}): ${err.message}. This is a host-side error \u2014 retry, or check the runtime with ${code("dashwise module status <slug>")}.`
|
|
10820
|
+
);
|
|
10821
|
+
return 1;
|
|
10822
|
+
}
|
|
10823
|
+
emit(err.code, `Could not read runtime logs: ${err.code} ${err.status} \u2014 ${err.message}`);
|
|
10824
|
+
return 1;
|
|
10825
|
+
}
|
|
10826
|
+
const message = `Could not read runtime logs: ${err.message}`;
|
|
10827
|
+
if (json) out(JSON.stringify({ ok: false, error: { code: "FETCH_FAILED", message } }));
|
|
10828
|
+
else fail(message);
|
|
10829
|
+
return 1;
|
|
10830
|
+
}
|
|
10831
|
+
async function sleep4(ms, aborted) {
|
|
10832
|
+
const slice = 100;
|
|
10833
|
+
let waited = 0;
|
|
10834
|
+
while (waited < ms && !aborted()) {
|
|
10835
|
+
await new Promise((resolve13) => setTimeout(resolve13, Math.min(slice, ms - waited)));
|
|
10836
|
+
waited += slice;
|
|
10837
|
+
}
|
|
10838
|
+
}
|
|
10839
|
+
|
|
10344
10840
|
// src/bin.ts
|
|
10345
10841
|
init_output();
|
|
10346
10842
|
var program = new Command();
|
|
@@ -10967,6 +11463,29 @@ moduleCmd.command("action-log").description(
|
|
|
10967
11463
|
});
|
|
10968
11464
|
}
|
|
10969
11465
|
);
|
|
11466
|
+
moduleCmd.command("logs [slug]").description(
|
|
11467
|
+
"Read a deployed module install's runtime logs (the host's log group). Resolves the install from the slug + workspace like `module status`, or target any install directly with `--install <id>`. `--since 15m|2h|<iso>` sets a lower bound; `--tail` shows the most-recent page; `--follow`/`-f` streams new lines every ~2s (Ctrl-C to stop). Workspace-admin."
|
|
11468
|
+
).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option(
|
|
11469
|
+
"--install <id>",
|
|
11470
|
+
"Target a specific installation id directly (deployed installs included). Find ids via `dashwise module status <slug>`. Skips slug + workspace resolution.",
|
|
11471
|
+
parseIntOption
|
|
11472
|
+
).option(
|
|
11473
|
+
"--since <iso-or-duration>",
|
|
11474
|
+
'Only lines newer than this. Accepts an ISO-8601 timestamp or a duration like "15m", "2h", "2d". Ignored with --tail.'
|
|
11475
|
+
).option("--limit <n>", "Max lines per fetch (1-1000, backend-clamped, default 100).", parseIntOption).option("--tail", "Show the most-recent page (ignores --since).").option("-f, --follow", "Poll every ~2s for new lines. Ctrl-C to stop. Mutually exclusive with --json.").option("--json", "Emit the raw `{ entries }` JSON response. Ignored with --follow.").action(
|
|
11476
|
+
async (slug, cmdOpts) => {
|
|
11477
|
+
process.exitCode = await moduleLogsCommand(slug, {
|
|
11478
|
+
...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
|
|
11479
|
+
...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
|
|
11480
|
+
...cmdOpts.install !== void 0 ? { install: cmdOpts.install } : {},
|
|
11481
|
+
...cmdOpts.since !== void 0 ? { since: cmdOpts.since } : {},
|
|
11482
|
+
...cmdOpts.limit !== void 0 ? { limit: cmdOpts.limit } : {},
|
|
11483
|
+
...cmdOpts.tail !== void 0 ? { tail: cmdOpts.tail } : {},
|
|
11484
|
+
...cmdOpts.follow !== void 0 ? { follow: cmdOpts.follow } : {},
|
|
11485
|
+
...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
|
|
11486
|
+
});
|
|
11487
|
+
}
|
|
11488
|
+
);
|
|
10970
11489
|
moduleCmd.command("destroy-dev-install").description(
|
|
10971
11490
|
"Destroy the local-dev install for the current module. Tears down the install row + dashboard tile; if the module had no other installs and was never published, drops the module too. Local module.json is preserved (only the cached runtime token + install id in auth.json get cleared)."
|
|
10972
11491
|
).option("--dir <path>", "Project root (default: current directory).").option(
|
|
@@ -11100,7 +11619,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
|
|
|
11100
11619
|
}
|
|
11101
11620
|
})();
|
|
11102
11621
|
function getVersion() {
|
|
11103
|
-
return "0.
|
|
11622
|
+
return "0.17.0";
|
|
11104
11623
|
}
|
|
11105
11624
|
function parseIntOption(value) {
|
|
11106
11625
|
const n = Number.parseInt(value, 10);
|