@descix/cli 1.0.8 → 1.0.10
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/agent-assets/instructions/onboarding-flow.md +6 -0
- package/agent-assets/reference/local-dev.md +41 -1
- package/agent-assets/reference/platform-api-consumer.md +11 -5
- package/agent-assets/reference/workspace-config.md +1 -1
- package/bin/descix.js +38 -141
- package/bin/mcp-server.js +24 -0
- package/lib/commands/dev-certs.js +93 -0
- package/lib/commands/doctor.js +31 -3
- package/lib/commands/media-upload.js +176 -0
- package/lib/dev-cert-resolver.js +50 -0
- package/lib/output.js +31 -0
- package/lib/verb-invokes.generated.json +2 -0
- package/package.json +3 -3
- package/templates/agent-claude.md +10 -0
- package/templates/agent-cline.md +10 -0
- package/templates/agent-copilot.md +10 -0
- package/templates/agent-cursor.md +10 -0
|
@@ -294,6 +294,12 @@ User is trying to configure an app they don't own. Suggest purchase.
|
|
|
294
294
|
### "Folder already configured"
|
|
295
295
|
Check if configuration matches intended app. Offer to update or skip.
|
|
296
296
|
|
|
297
|
+
### Powch sign-in under `descix serve` fails ("User cancelled", or "WebAuthn is not supported on sites with TLS certificate errors")
|
|
298
|
+
The gateway's dev certificate is not trusted. Run `descix dev-certs check`. If it is not trusted,
|
|
299
|
+
give the user exactly `descix dev-certs trust` to run (macOS password prompt), tell them to quit
|
|
300
|
+
and reopen Chrome, then run `descix dev-certs check` again. Details and Linux/Windows notes:
|
|
301
|
+
`reference/local-dev.md` §3.3.
|
|
302
|
+
|
|
297
303
|
## Next Steps After Onboarding
|
|
298
304
|
|
|
299
305
|
Once configured, guide user to:
|
|
@@ -187,11 +187,51 @@ a different case and FAILS LOUD naming the remedy, rather than quietly becoming
|
|
|
187
187
|
|
|
188
188
|
The derived Shell rule is the important one: point the API at a cloud environment and the shell comes from that same origin, so one origin carries shell + app + `/apifront` with nothing else configured. A stale `env.platform.site` block does not hijack the root once the API is remote. Platform developers opt IN to a local shell by naming it (`--site-url https://localhost:5174` or `env.siteUrl`).
|
|
189
189
|
|
|
190
|
+
### 3.3. The dev certificate and passkey sign-in
|
|
191
|
+
|
|
192
|
+
The gateway's HTTPS uses one certificate: the pair named by `env.devCerts`
|
|
193
|
+
(`descix config set-dev-certs`), else the SDK's shipped self-signed pair (SAN `localhost`,
|
|
194
|
+
`127.0.0.1`, `::1`). The same pair serves every app dev server behind the gateway.
|
|
195
|
+
|
|
196
|
+
**Chrome refuses WebAuthn on a tab whose certificate is untrusted, and it judges the whole tab.**
|
|
197
|
+
The Powch sign-in iframe on `powch.descix.net` therefore fails inside an untrusted
|
|
198
|
+
`https://localhost:<port>` tab, even though Powch's own origin is trusted.
|
|
199
|
+
|
|
200
|
+
| Symptom | Meaning |
|
|
201
|
+
|---|---|
|
|
202
|
+
| Powch/ChatWidget log shows only `User cancelled` on sign-in | Powch's panel-close rejection; the real error is in the browser console |
|
|
203
|
+
| Browser console: `NotAllowedError: WebAuthn is not supported on sites with TLS certificate errors` | the gateway certificate is not trusted (or has expired) |
|
|
204
|
+
| `descix serve` banner: `Passkey sign-in will fail: <reason>. Run: descix dev-certs trust` | same, detected before the browser opens |
|
|
205
|
+
| `descix doctor` row "Dev certificate" not passing | same |
|
|
206
|
+
|
|
207
|
+
| Command | Who runs it | Effect |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| `descix dev-certs check [--json]` | the assisting agent | resolves the cert the gateway uses; exits 0 only when it is trusted; reports `unverifiable` with the reason on Linux and Windows |
|
|
210
|
+
| `descix doctor` | the assisting agent | includes the same result as the "Dev certificate" check |
|
|
211
|
+
| `descix dev-certs trust` | **the human** | macOS: trusts that cert in the login keychain (password prompt), re-checks, and says to quit and reopen Chrome |
|
|
212
|
+
|
|
213
|
+
**Assisting-agent flow:**
|
|
214
|
+
|
|
215
|
+
1. Run `descix doctor` (or `descix dev-certs check`).
|
|
216
|
+
2. If the dev certificate is trusted, continue. Passkey failures then have another cause.
|
|
217
|
+
3. If it is not trusted, hand the human exactly one command to run in their own terminal:
|
|
218
|
+
`descix dev-certs trust`. It needs their macOS login password, so the agent does not run it.
|
|
219
|
+
4. When they report it succeeded, tell them to **quit Chrome completely and reopen it** (a running
|
|
220
|
+
Chrome keeps its old verdict), reload the `descix serve` URL, and sign in again.
|
|
221
|
+
5. Run `descix dev-certs check` again and confirm exit 0 before calling sign-in fixed.
|
|
222
|
+
|
|
223
|
+
**Linux and Windows:** the check reports `unverifiable`, and `descix dev-certs trust` is macOS-only.
|
|
224
|
+
Trusting the cert is a manual step that has not been verified on DeSciX: import the gateway's
|
|
225
|
+
`cert.pem` into the store Chrome reads (Linux: the NSS database,
|
|
226
|
+
`certutil -d sql:$HOME/.pki/nssdb -A -t "P,," -n descix-localhost -i <cert.pem>`; Windows: the
|
|
227
|
+
Current User "Trusted Root Certification Authorities" store), then restart the browser. Tell the
|
|
228
|
+
human it is unverified.
|
|
229
|
+
|
|
190
230
|
## 4. Scenarios
|
|
191
231
|
|
|
192
232
|
### Scenario 1: App Dev (the common case)
|
|
193
233
|
* **User**: App developer. No platform checkout.
|
|
194
|
-
* **Setup**: `descix config set-env dev`, `descix app init`, `descix app set-site`, then `cd my-app && descix serve`.
|
|
234
|
+
* **Setup**: `descix config set-env dev`, `descix app init`, `descix app set-site`, `descix dev-certs check` (if not trusted, the human runs `descix dev-certs trust` once — §3.3), then `cd my-app && descix serve`.
|
|
195
235
|
* **Result**: one HTTPS origin. `/` → the cloud App Shell, `/apifront` → the cloud API, `/p/my-app` → your app, and the shell boots **standalone into your app** — its INITIAL VIEW is your app rather than the store, with the platform views still reachable from there.
|
|
196
236
|
|
|
197
237
|
### Scenario 2: Platform Dev ("Dogfooding")
|
|
@@ -91,11 +91,17 @@ ASSET NOT FOUND at gs://... Upload it first with `descix app media-upload`.
|
|
|
91
91
|
Both come back as `MEDIA_ASSET_UNREADABLE`. The first means you referenced the wrong app's asset;
|
|
92
92
|
the second means you have not uploaded yet. Upload into the app that will do the asking.
|
|
93
93
|
|
|
94
|
-
**The upload verb hands you
|
|
95
|
-
`path` (relative to the app's assets prefix)
|
|
96
|
-
`public_url
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
**The upload verb hands you the relative `path`, and it is a valid `asset_ref` as-is.** `descix
|
|
95
|
+
app media-upload` returns, per file, a `path` (relative to the app's assets prefix), a
|
|
96
|
+
`public_url` (the app-host form, e.g. `https://{app}.descix.net/assets/<path>`), `gcs_path` and
|
|
97
|
+
`content_type`; `--json` prints them for scripting. Pass `path` (or a `gs://` URI you construct
|
|
98
|
+
yourself) as `asset_ref` — either form works, see below.
|
|
99
|
+
|
|
100
|
+
**Who can upload, and how much.** Any app owner, community admin or platform admin may call
|
|
101
|
+
`media-upload` for that app. Storage is capped **200 MB per app today** (pending credit-based
|
|
102
|
+
metering for more) — the cap is enforced server-side by `get_asset_upload_token`, which refuses
|
|
103
|
+
with its own message when a would-be upload exceeds it; the CLI never pre-checks quota locally,
|
|
104
|
+
so trust that refusal over any local math.
|
|
99
105
|
|
|
100
106
|
**The `gs://` form is accepted.** A `gs://` URI works as an `asset_ref` — it is parsed and
|
|
101
107
|
scope-checked, and the handler then looks the object up by path. What is PROVEN is the form and
|
|
@@ -33,7 +33,7 @@ is the schema and the verb that owns each key.
|
|
|
33
33
|
| `env.environment` | `DEV`, `DEMO` or `PROD` | `descix config init --env dev\|demo\|prod` |
|
|
34
34
|
| `env.apiUrl` | the API origin this workspace talks to | `descix config init --env …` (known envs) / `descix config set-env <name> --url <origin>` (custom) |
|
|
35
35
|
| `env.gateway.port` | the port `descix serve` listens on | `descix config set-gateway-port <port>` |
|
|
36
|
-
| `env.devCerts` | `dir`, or `cert` + `key` — the TLS pair for the gateway and every app behind it | `descix config set-dev-certs --dir\|--cert\|--key\|--clear` |
|
|
36
|
+
| `env.devCerts` | `dir`, or `cert` + `key` — the TLS pair for the gateway and every app behind it; unset means the SDK's shipped pair. Passkey sign-in needs it trusted: `descix dev-certs check`, then the human runs `descix dev-certs trust` (see `reference/local-dev.md` §3.3) | `descix config set-dev-certs --dir\|--cert\|--key\|--clear` |
|
|
37
37
|
| `env.powchUrl` | Powch's own origin (optional; Powch is cross-origin from the shell by design) | `descix config set-powch-url <url>` |
|
|
38
38
|
| `env.siteUrl` | the App Shell origin the gateway proxies `/` to (optional; default is the API origin) | `descix config set-site-url <url>` |
|
|
39
39
|
| `env.products[]` | one entry per app in this workspace, see below | `descix app init`, `app set-site`, `app set-port`, `app set-localpath`, `app unmap` |
|
package/bin/descix.js
CHANGED
|
@@ -40,35 +40,12 @@ import { kbVectorCell, kbCountSource } from '../lib/commands/kb-list-render.js';
|
|
|
40
40
|
import * as corpusCommands from '../lib/commands/corpus.js';
|
|
41
41
|
import * as modelConfigCommands from '../lib/commands/model-config.js';
|
|
42
42
|
import { isHelpInvocation, getCommandSurface, applyVisibility } from '../lib/command-visibility.js';
|
|
43
|
+
import { progress } from '../lib/output.js';
|
|
44
|
+
import { runMediaUpload } from '../lib/commands/media-upload.js';
|
|
43
45
|
import fs from 'fs/promises';
|
|
44
46
|
import path from 'path';
|
|
45
47
|
import os from 'os';
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Emit human-facing PROGRESS for a command that can also emit JSON.
|
|
50
|
-
*
|
|
51
|
-
* When `--json` is set, stdout is a DATA CHANNEL: it must contain the JSON document and
|
|
52
|
-
* nothing else, or every scripted consumer breaks. Progress then goes to stderr, where a
|
|
53
|
-
* human piping stdout still sees it. Without `--json`, stdout is the human channel and
|
|
54
|
-
* progress belongs there.
|
|
55
|
-
*
|
|
56
|
-
* ONE OWNER of that decision. Before this existed, each --json command re-decided it by
|
|
57
|
-
* calling console.log directly, and both of them got it wrong: `mcp execute --json` prefixed
|
|
58
|
-
* the document with "Executing <tool>..." and `app media-upload --json` prefixed it with a
|
|
59
|
-
* header, a per-file listing and a per-file +/x line. Both exited 0, so a consumer saw a
|
|
60
|
-
* success code and an unparseable stream. Measured 2026-08-21/22 (seat BEAST): two seats had
|
|
61
|
-
* independently hand-rolled prefix-stripping workarounds (`tail -n +2`, `raw.find('[')`)
|
|
62
|
-
* rather than reporting it.
|
|
63
|
-
*
|
|
64
|
-
* @param {Object} options - the command's parsed options (read for .json)
|
|
65
|
-
* @param {...any} args - passed through to console.log / console.error
|
|
66
|
-
*/
|
|
67
|
-
function progress(options, ...args) {
|
|
68
|
-
if (options && options.json) console.error(...args);
|
|
69
|
-
else console.log(...args);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
49
|
/**
|
|
73
50
|
* THE ONE OWNER of "a command threw, and the process must stop saying why".
|
|
74
51
|
*
|
|
@@ -1196,15 +1173,12 @@ appCommand
|
|
|
1196
1173
|
|
|
1197
1174
|
|
|
1198
1175
|
// descix app media-upload — upload media/asset files to an app's GCS assets prefix via the
|
|
1199
|
-
// API surface (WS-V1-PURGE Phase 1, item 2; media-via-API-surface PLATFORM half).
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
// (`get_asset_upload_token` over /apifront, user-session authed), upload each file straight
|
|
1203
|
-
// to GCS, and print the ASSET REFERENCES. An app microservice then fetches an uploaded asset
|
|
1204
|
-
// by reference over the Core broker via `get_app_asset` (no shared local filesystem needed).
|
|
1176
|
+
// API surface (WS-V1-PURGE Phase 1, item 2; media-via-API-surface PLATFORM half). Command body
|
|
1177
|
+
// lives in lib/commands/media-upload.js (runMediaUpload) — see that module for the full
|
|
1178
|
+
// contract (server-owned quota/path/permission checks, verbatim upload_headers ferrying).
|
|
1205
1179
|
appCommand
|
|
1206
1180
|
.command('media-upload')
|
|
1207
|
-
.description('Upload media/asset files to an app\'s GCS assets prefix
|
|
1181
|
+
.description('Upload media/asset files to an app\'s GCS assets prefix (200 MB per app today; app owners, community admins and platform admins may upload — returns asset references)')
|
|
1208
1182
|
.requiredOption('-a, --app <id>', 'App ID (community is resolved server-side from Products)')
|
|
1209
1183
|
.requiredOption('-f, --file <path...>', 'One or more local file paths to upload')
|
|
1210
1184
|
.option('--prefix <relPath>', 'Optional sub-path under the app assets/ prefix (e.g. "shows/myshow")', '')
|
|
@@ -1213,115 +1187,7 @@ appCommand
|
|
|
1213
1187
|
try {
|
|
1214
1188
|
const apiClient = new DeSciXApiClient();
|
|
1215
1189
|
await requireAuth(apiClient);
|
|
1216
|
-
|
|
1217
|
-
const path = await import('path');
|
|
1218
|
-
const mime = (await import('mime-types')).default;
|
|
1219
|
-
|
|
1220
|
-
const appId = options.app;
|
|
1221
|
-
const localFiles = Array.isArray(options.file) ? options.file : [options.file];
|
|
1222
|
-
const subPrefix = (options.prefix || '').replace(/^\/+|\/+$/g, '');
|
|
1223
|
-
|
|
1224
|
-
// Build the upload descriptor list: object path under assets/ = [subPrefix/]basename.
|
|
1225
|
-
const fileDescriptors = [];
|
|
1226
|
-
for (const localPath of localFiles) {
|
|
1227
|
-
const abs = path.resolve(localPath);
|
|
1228
|
-
let stat;
|
|
1229
|
-
try {
|
|
1230
|
-
stat = await fs.stat(abs);
|
|
1231
|
-
} catch {
|
|
1232
|
-
console.error(chalk.red(`\n❌ File not found: ${abs}\n`));
|
|
1233
|
-
process.exit(1);
|
|
1234
|
-
}
|
|
1235
|
-
if (!stat.isFile()) {
|
|
1236
|
-
console.error(chalk.red(`\n❌ Not a file: ${abs}\n`));
|
|
1237
|
-
process.exit(1);
|
|
1238
|
-
}
|
|
1239
|
-
const base = path.basename(abs);
|
|
1240
|
-
const objectPath = subPrefix ? `${subPrefix}/${base}` : base;
|
|
1241
|
-
fileDescriptors.push({
|
|
1242
|
-
path: objectPath,
|
|
1243
|
-
content_type: mime.lookup(abs) || 'application/octet-stream',
|
|
1244
|
-
size: stat.size,
|
|
1245
|
-
_absolutePath: abs
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
console.log(chalk.cyan(`\n Media Upload: ${appId} → GCS assets/\n`));
|
|
1250
|
-
fileDescriptors.forEach(f => progress(options, chalk.gray(` • ${f.path} (${(f.size / 1024).toFixed(1)}KB, ${f.content_type})`)));
|
|
1251
|
-
|
|
1252
|
-
// 1. Request a signed-PUT upload token over the API surface.
|
|
1253
|
-
const tokenResponse = await apiClient.invoke('get_asset_upload_token', {
|
|
1254
|
-
app_id: appId,
|
|
1255
|
-
files: fileDescriptors.map(f => ({ path: f.path, content_type: f.content_type, size: f.size }))
|
|
1256
|
-
});
|
|
1257
|
-
const token = tokenResponse.message || tokenResponse;
|
|
1258
|
-
const { signed_urls, objects } = token;
|
|
1259
|
-
|
|
1260
|
-
// 2. PUT each file directly to GCS using its signed URL.
|
|
1261
|
-
progress(options, chalk.gray(`\n Uploading ${fileDescriptors.length} file(s)...`));
|
|
1262
|
-
const uploaded = [];
|
|
1263
|
-
const errors = [];
|
|
1264
|
-
for (const f of fileDescriptors) {
|
|
1265
|
-
const signedUrl = signed_urls?.[f.path];
|
|
1266
|
-
if (!signedUrl) {
|
|
1267
|
-
errors.push(`No signed URL for: ${f.path}`);
|
|
1268
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
try {
|
|
1272
|
-
const content = await fs.readFile(f._absolutePath);
|
|
1273
|
-
const resp = await fetch(signedUrl, {
|
|
1274
|
-
method: 'PUT',
|
|
1275
|
-
headers: { 'Content-Type': f.content_type },
|
|
1276
|
-
body: content
|
|
1277
|
-
});
|
|
1278
|
-
if (!resp.ok) {
|
|
1279
|
-
errors.push(`Failed ${f.path}: ${resp.status} ${resp.statusText}`);
|
|
1280
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1281
|
-
} else {
|
|
1282
|
-
const obj = (objects || []).find(o => o.path === f.path) || {};
|
|
1283
|
-
// The asset REFERENCE an app handler / get_app_asset consumes: the gs:// URI.
|
|
1284
|
-
const gcsRef = obj.gcs_path
|
|
1285
|
-
? `gs://${token.bucket}/${obj.gcs_path}`
|
|
1286
|
-
: null;
|
|
1287
|
-
uploaded.push({
|
|
1288
|
-
path: f.path,
|
|
1289
|
-
ref: gcsRef,
|
|
1290
|
-
gcs_path: obj.gcs_path || null,
|
|
1291
|
-
public_url: obj.public_url || null,
|
|
1292
|
-
content_type: f.content_type,
|
|
1293
|
-
size: f.size
|
|
1294
|
-
});
|
|
1295
|
-
progress(options, chalk.green(` + ${f.path}`));
|
|
1296
|
-
}
|
|
1297
|
-
} catch (err) {
|
|
1298
|
-
errors.push(`Error ${f.path}: ${err.message}`);
|
|
1299
|
-
progress(options, chalk.red(` x ${f.path}`));
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
if (errors.length > 0) {
|
|
1304
|
-
progress(options, chalk.yellow(`\n ${errors.length} error(s):`));
|
|
1305
|
-
errors.forEach(e => progress(options, chalk.red(` - ${e}`)));
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
if (uploaded.length === 0) {
|
|
1309
|
-
console.error(chalk.red('\n❌ No files uploaded.\n'));
|
|
1310
|
-
process.exit(1);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
if (options.json) {
|
|
1314
|
-
console.log('\n' + JSON.stringify({ app_id: appId, assets: uploaded }, null, 2) + '\n');
|
|
1315
|
-
} else {
|
|
1316
|
-
console.log(chalk.green(`\n ✅ Uploaded ${uploaded.length} asset(s).\n`));
|
|
1317
|
-
console.log(chalk.cyan(' Asset references (pass these to your app handler; the service fetches via get_app_asset):'));
|
|
1318
|
-
uploaded.forEach(u => {
|
|
1319
|
-
console.log(chalk.white(` ${u.path}`));
|
|
1320
|
-
console.log(chalk.gray(` ref: ${u.ref}`));
|
|
1321
|
-
console.log(chalk.gray(` public_url: ${u.public_url}`));
|
|
1322
|
-
});
|
|
1323
|
-
console.log();
|
|
1324
|
-
}
|
|
1190
|
+
await runMediaUpload(apiClient, options);
|
|
1325
1191
|
} catch (error) {
|
|
1326
1192
|
console.error(chalk.red(`\n❌ ${error.message}\n`));
|
|
1327
1193
|
process.exit(1);
|
|
@@ -4627,6 +4493,37 @@ program
|
|
|
4627
4493
|
}
|
|
4628
4494
|
});
|
|
4629
4495
|
|
|
4496
|
+
// ============ Dev Certs Commands ============
|
|
4497
|
+
|
|
4498
|
+
const devCertsCommand = program
|
|
4499
|
+
.command('dev-certs')
|
|
4500
|
+
.description('TLS dev-cert trust status for `descix serve` — passkey sign-in needs this cert trusted once per machine');
|
|
4501
|
+
|
|
4502
|
+
devCertsCommand
|
|
4503
|
+
.command('check')
|
|
4504
|
+
.description('Check whether the dev-server cert is trusted for https://localhost (exit 0 only when trusted)')
|
|
4505
|
+
.option('--json', 'Output raw JSON')
|
|
4506
|
+
.action(async (options) => {
|
|
4507
|
+
try {
|
|
4508
|
+
const { runDevCertsCheck } = await import('../lib/commands/dev-certs.js');
|
|
4509
|
+
await runDevCertsCheck(options);
|
|
4510
|
+
} catch (error) {
|
|
4511
|
+
fail(error);
|
|
4512
|
+
}
|
|
4513
|
+
});
|
|
4514
|
+
|
|
4515
|
+
devCertsCommand
|
|
4516
|
+
.command('trust')
|
|
4517
|
+
.description('Trust the dev-server cert in the macOS keychain (prompts for your password)')
|
|
4518
|
+
.action(async () => {
|
|
4519
|
+
try {
|
|
4520
|
+
const { runDevCertsTrust } = await import('../lib/commands/dev-certs.js');
|
|
4521
|
+
await runDevCertsTrust();
|
|
4522
|
+
} catch (error) {
|
|
4523
|
+
fail(error);
|
|
4524
|
+
}
|
|
4525
|
+
});
|
|
4526
|
+
|
|
4630
4527
|
// ============ Quickstart Command ============
|
|
4631
4528
|
|
|
4632
4529
|
program
|
package/bin/mcp-server.js
CHANGED
|
@@ -21,6 +21,8 @@ import { WorkspaceConfig } from '../lib/workspace-config.js';
|
|
|
21
21
|
import { DeSciXApiClient } from '../lib/api-client.js';
|
|
22
22
|
import { WalletFileManager } from '../lib/wallet-file.js';
|
|
23
23
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
24
|
+
import { checkDevCert } from '@descix/app-sdk/dev';
|
|
25
|
+
import { resolveGatewayCertContext } from '../lib/dev-cert-resolver.js';
|
|
24
26
|
import * as fs from 'fs/promises';
|
|
25
27
|
import * as path from 'path';
|
|
26
28
|
// WS-MCP-SSOT-TIER2 (audit §5.B-1): the curated HTTP-valid core tools are the SINGLE SOURCE
|
|
@@ -322,6 +324,28 @@ try {
|
|
|
322
324
|
}
|
|
323
325
|
}
|
|
324
326
|
|
|
327
|
+
// Dev certificate — the ONE owner (checkDevCert, shared with `descix
|
|
328
|
+
// dev-certs`, `descix doctor` and the `descix serve` banner). An untrusted
|
|
329
|
+
// cert is invisible from inside a WebAuthn ceremony: Chrome judges the
|
|
330
|
+
// whole tab's TLS state and Powch only ever sees "User cancelled", so an
|
|
331
|
+
// agent assisting a stuck developer needs this named explicitly, not
|
|
332
|
+
// inferred from a login failure.
|
|
333
|
+
try {
|
|
334
|
+
const { certPath } = resolveGatewayCertContext(checkRoot);
|
|
335
|
+
const certResult = checkDevCert({ certPath });
|
|
336
|
+
report.dev_cert = { cert_path: certPath, ...certResult };
|
|
337
|
+
if (certResult.status !== 'trusted') {
|
|
338
|
+
report.warnings.push(
|
|
339
|
+
'Dev certificate not trusted: passkey sign-in under descix serve will fail ' +
|
|
340
|
+
'(Powch logs "User cancelled"; the console says "WebAuthn is not supported on sites ' +
|
|
341
|
+
'with TLS certificate errors"). Ask the user to run exactly: descix dev-certs trust ' +
|
|
342
|
+
'— then quit and reopen Chrome, and re-run descix dev-certs check.'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
report.dev_cert = { status: 'unverifiable', detail: `Could not resolve dev cert: ${err.message}`, next: null };
|
|
347
|
+
}
|
|
348
|
+
|
|
325
349
|
// Suggest cloud brain alignment when workspace is configured but alignment is missing
|
|
326
350
|
if (!report.alignment && report.workspace?.configured) {
|
|
327
351
|
report.suggestions = report.suggestions || [];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev-certs — report and (on trust) apply this machine's TLS trust status for
|
|
3
|
+
* the cert `descix serve` uses for its localhost HTTPS origin.
|
|
4
|
+
*
|
|
5
|
+
* Cert resolution is the ONE owner shared with `descix doctor` and the
|
|
6
|
+
* `descix serve` banner: lib/dev-cert-resolver.js -> @descix/app-sdk/dev's
|
|
7
|
+
* resolveDevCertOptions + resolveCertPaths. This file never re-derives which
|
|
8
|
+
* cert is "the" dev cert, and never mints or replaces one — the shipped
|
|
9
|
+
* SDK-tracked pair (descix-app-sdk/src/dev/certs/) is the only cert this
|
|
10
|
+
* command ever points at, same as every other consumer.
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { checkDevCert, trustCertCommand } from '@descix/app-sdk/dev';
|
|
15
|
+
import { resolveGatewayCertContext } from '../dev-cert-resolver.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `descix dev-certs check [--json]` — exits 0 only when the cert is trusted.
|
|
19
|
+
*/
|
|
20
|
+
export async function runDevCertsCheck(options = {}) {
|
|
21
|
+
const { certPath, port } = resolveGatewayCertContext(process.cwd());
|
|
22
|
+
const result = checkDevCert({ certPath });
|
|
23
|
+
|
|
24
|
+
if (options.json) {
|
|
25
|
+
console.log(JSON.stringify({ certPath, port, ...result }, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
console.log(chalk.cyan('\nDev certificate\n'));
|
|
28
|
+
console.log(` Cert: ${certPath}`);
|
|
29
|
+
console.log(` Status: ${result.status === 'trusted' ? chalk.green(result.status) : chalk.red(result.status)}`);
|
|
30
|
+
console.log(` Detail: ${result.detail}`);
|
|
31
|
+
if (result.next) {
|
|
32
|
+
console.log(`\n Next:\n ${String(result.next).replace(/\n/g, '\n ')}`);
|
|
33
|
+
}
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
process.exitCode = result.status === 'trusted' ? 0 : 1;
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* `descix dev-certs trust` — the ONE final human command. Runs the exact
|
|
43
|
+
* trustCertCommand() string (macOS prompts for the keychain password), then
|
|
44
|
+
* re-checks and tells the human to reload Chrome. Refuses to run against a
|
|
45
|
+
* cert that is structurally broken (missing / no SAN / expired) or on a
|
|
46
|
+
* platform this cannot verify — those are not this command's job to paper
|
|
47
|
+
* over, and running `security add-trusted-cert` on a bad cert would silently
|
|
48
|
+
* "succeed" while passkeys still fail.
|
|
49
|
+
*/
|
|
50
|
+
export async function runDevCertsTrust() {
|
|
51
|
+
const { certPath, port } = resolveGatewayCertContext(process.cwd());
|
|
52
|
+
const before = checkDevCert({ certPath });
|
|
53
|
+
|
|
54
|
+
if (before.status === 'trusted') {
|
|
55
|
+
console.log(chalk.green(`\n${certPath} is already trusted.\n`));
|
|
56
|
+
return before;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (before.status === 'unverifiable') {
|
|
60
|
+
// `security add-trusted-cert` is macOS-only — there is no automated trust
|
|
61
|
+
// step on this platform. Refuse loud rather than silently no-op, and hand
|
|
62
|
+
// the human the exact file so they can import it into their own OS/browser
|
|
63
|
+
// trust store by hand.
|
|
64
|
+
console.error(chalk.red(`\nCannot trust automatically on ${process.platform}: ${before.detail}`));
|
|
65
|
+
console.error(`\nImport this certificate into your OS/browser trust store manually:\n ${certPath}\n`);
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return before;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (before.status !== 'untrusted') {
|
|
71
|
+
console.error(chalk.red(`\nCannot trust this cert — status: ${before.status}`));
|
|
72
|
+
console.error(` ${before.detail}`);
|
|
73
|
+
if (before.next) {
|
|
74
|
+
console.error(`\n Next:\n ${String(before.next).replace(/\n/g, '\n ')}\n`);
|
|
75
|
+
}
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return before;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(chalk.cyan('\nTrusting the dev certificate — macOS will prompt for your password.\n'));
|
|
81
|
+
execSync(trustCertCommand(certPath), { stdio: 'inherit' });
|
|
82
|
+
|
|
83
|
+
const after = checkDevCert({ certPath });
|
|
84
|
+
if (after.status === 'trusted') {
|
|
85
|
+
console.log(chalk.green('\nTrusted.'));
|
|
86
|
+
console.log(`Quit and reopen Chrome, then reload https://localhost:${port}/\n`);
|
|
87
|
+
} else {
|
|
88
|
+
console.error(chalk.red(`\nStill not trusted after running the trust command (${after.status}).`));
|
|
89
|
+
console.error(` ${after.detail}\n`);
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
return after;
|
|
93
|
+
}
|
package/lib/commands/doctor.js
CHANGED
|
@@ -14,6 +14,8 @@ import { exec } from 'child_process';
|
|
|
14
14
|
import { promisify } from 'util';
|
|
15
15
|
import { DeSciXApiClient } from '../api-client.js';
|
|
16
16
|
import { isAuthenticated } from '../auth-guard.js';
|
|
17
|
+
import { checkDevCert } from '@descix/app-sdk/dev';
|
|
18
|
+
import { resolveGatewayCertContext } from '../dev-cert-resolver.js';
|
|
17
19
|
|
|
18
20
|
const execAsync = promisify(exec);
|
|
19
21
|
|
|
@@ -223,6 +225,26 @@ export async function checkAdc() {
|
|
|
223
225
|
}
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Check the TLS cert `descix serve` would load for the cwd. An untrusted or
|
|
230
|
+
* structurally broken cert breaks passkey (WebAuthn) sign-in — the only
|
|
231
|
+
* DeSciX auth — so this is a real failure, not an admin-only advisory.
|
|
232
|
+
* Resolution mirrors the gateway exactly (dev-cert-resolver.js, the ONE
|
|
233
|
+
* owner shared with `descix dev-certs` and the `descix serve` banner).
|
|
234
|
+
*/
|
|
235
|
+
async function checkDevCertificate() {
|
|
236
|
+
const { certPath } = resolveGatewayCertContext(process.cwd());
|
|
237
|
+
const result = checkDevCert({ certPath });
|
|
238
|
+
if (result.status === 'trusted') {
|
|
239
|
+
return { ok: true, message: `${certPath} (trusted)` };
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
message: `${result.status} — ${result.detail}`,
|
|
244
|
+
remediation: result.next || 'Run: descix dev-certs check',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
226
248
|
/**
|
|
227
249
|
* Run the doctor command
|
|
228
250
|
*/
|
|
@@ -282,7 +304,12 @@ export async function runDoctor(options = {}) {
|
|
|
282
304
|
spinner.text = 'Checking ADC Credentials...';
|
|
283
305
|
const adcRes = await checkAdc();
|
|
284
306
|
results.push({ label: 'ADC Credentials', ...adcRes });
|
|
285
|
-
|
|
307
|
+
|
|
308
|
+
// 7. Dev certificate (passkey sign-in under `descix serve`)
|
|
309
|
+
spinner.text = 'Checking dev certificate...';
|
|
310
|
+
const devCertRes = await checkDevCertificate();
|
|
311
|
+
results.push({ label: 'Dev certificate', ...devCertRes });
|
|
312
|
+
|
|
286
313
|
spinner.stop();
|
|
287
314
|
|
|
288
315
|
// Print Results
|
|
@@ -317,8 +344,9 @@ export async function runDoctor(options = {}) {
|
|
|
317
344
|
// the pass stops.
|
|
318
345
|
console.log(chalk.gray(
|
|
319
346
|
'Checked: Node version, API origin resolution + reachability, session, local write\n' +
|
|
320
|
-
'permissions
|
|
321
|
-
'checked: your app\'s own build, chain state, or any
|
|
347
|
+
'permissions, dev-cert trust for `descix serve`. [-] rows are admin-only dependencies and\n' +
|
|
348
|
+
'never fail this command. NOT checked: your app\'s own build, chain state, or any\n' +
|
|
349
|
+
'credential\'s permissions at the API.'
|
|
322
350
|
));
|
|
323
351
|
console.log();
|
|
324
352
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `descix app media-upload` — upload media/asset files to an app's GCS assets prefix via the
|
|
3
|
+
* API surface (WS-V1-PURGE Phase 1, item 2; media-via-API-surface PLATFORM half; storage-cap
|
|
4
|
+
* ferrying added WS-MEDIA-QUOTA per CEO ruling 2026-09-16: "Yes you can build with a 200MB
|
|
5
|
+
* size limit for now until we get credit based metering for more storage.").
|
|
6
|
+
*
|
|
7
|
+
* This is the canonical, filesystem-free way to get app media (podcast audio, cover art, etc.)
|
|
8
|
+
* into the platform: request a short-lived signed PUT token from the API surface
|
|
9
|
+
* (`get_asset_upload_token` over /apifront, user-session authed), upload each file straight to
|
|
10
|
+
* GCS, and print the server's asset references. An app microservice then fetches an uploaded
|
|
11
|
+
* asset by reference over the Core broker via `get_app_asset` (no shared local filesystem
|
|
12
|
+
* needed). App owners, community admins and platform admins may upload — the server is the
|
|
13
|
+
* sole authority on that and on the app's storage quota (200 MB per app today; nothing here
|
|
14
|
+
* pre-validates either).
|
|
15
|
+
*
|
|
16
|
+
* SERVER IS THE SOLE AUTHORITY (super-DRY: never re-derive a server-owned fact client-side):
|
|
17
|
+
* - quota, path validity (no leading `/`, no backslash, no empty/`.`/`..` segment) and caller
|
|
18
|
+
* permission are enforced ONLY by `get_asset_upload_token`; a refusal there throws with the
|
|
19
|
+
* server's message verbatim (`DeSciXApiClient#invoke`) and this module does not catch it —
|
|
20
|
+
* let it propagate so the caller prints exactly what the server said and exits non-zero.
|
|
21
|
+
* - `--prefix` is passed through UNCHANGED (no slash-stripping): stripping could silently mask
|
|
22
|
+
* a server-side path refusal the server is best positioned to name.
|
|
23
|
+
* - Each file is PUT with the server's `upload_headers[path]` bag VERBATIM (precedent: `site
|
|
24
|
+
* upload`'s `ferriedHeaders` — the server binds Content-Type and
|
|
25
|
+
* `x-goog-content-length-range` into the v4 signed-URL signature, so a re-derived header
|
|
26
|
+
* 403s). Size is still read from `fs.stat`, since GCS itself rejects a body whose length
|
|
27
|
+
* disagrees with the declared range.
|
|
28
|
+
* - `public_url` and `storage` are printed exactly as the server returned them — never
|
|
29
|
+
* reconstructed from `bucket` + `gcs_path` (that WAS a client-side re-derivation of a server
|
|
30
|
+
* fact; removed here rather than perpetuated).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import * as fs from 'fs/promises';
|
|
34
|
+
import * as path from 'path';
|
|
35
|
+
import chalk from 'chalk';
|
|
36
|
+
import { progress } from '../output.js';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run `app media-upload`.
|
|
40
|
+
*
|
|
41
|
+
* @param {import('../api-client.js').DeSciXApiClient} apiClient
|
|
42
|
+
* @param {{app:string, file:string|string[], prefix?:string, json?:boolean}} options
|
|
43
|
+
* @returns {Promise<{app_id:string, assets:Array<object>, storage:object|null}>}
|
|
44
|
+
*/
|
|
45
|
+
export async function runMediaUpload(apiClient, options) {
|
|
46
|
+
const mime = (await import('mime-types')).default;
|
|
47
|
+
|
|
48
|
+
const appId = options.app;
|
|
49
|
+
const localFiles = Array.isArray(options.file) ? options.file : [options.file];
|
|
50
|
+
// No local normalization: a leading/trailing slash, empty segment, or `.`/`..` segment is a
|
|
51
|
+
// server-owned validity rule (get_asset_upload_token) — pass --prefix through untouched so a
|
|
52
|
+
// bad prefix surfaces as the server's own refusal, never a silently-corrected local guess.
|
|
53
|
+
const subPrefix = options.prefix || '';
|
|
54
|
+
|
|
55
|
+
// Build the upload descriptor list: object path under assets/ = [subPrefix/]basename.
|
|
56
|
+
const fileDescriptors = [];
|
|
57
|
+
for (const localPath of localFiles) {
|
|
58
|
+
const abs = path.resolve(localPath);
|
|
59
|
+
let stat;
|
|
60
|
+
try {
|
|
61
|
+
stat = await fs.stat(abs);
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`File not found: ${abs}`);
|
|
64
|
+
}
|
|
65
|
+
if (!stat.isFile()) {
|
|
66
|
+
throw new Error(`Not a file: ${abs}`);
|
|
67
|
+
}
|
|
68
|
+
const base = path.basename(abs);
|
|
69
|
+
const objectPath = subPrefix ? `${subPrefix}/${base}` : base;
|
|
70
|
+
fileDescriptors.push({
|
|
71
|
+
path: objectPath,
|
|
72
|
+
content_type: mime.lookup(abs) || 'application/octet-stream',
|
|
73
|
+
size: stat.size,
|
|
74
|
+
_absolutePath: abs
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
progress(options, chalk.cyan(`\n Media Upload: ${appId} → GCS assets/\n`));
|
|
79
|
+
fileDescriptors.forEach(f => progress(options, chalk.gray(` • ${f.path} (${(f.size / 1024).toFixed(1)}KB, ${f.content_type})`)));
|
|
80
|
+
|
|
81
|
+
// 1. Request a signed-PUT upload token over the API surface. The server owns quota, path
|
|
82
|
+
// validity and caller permission — a refusal here throws with the server's message verbatim
|
|
83
|
+
// and this function does not catch it.
|
|
84
|
+
const tokenResponse = await apiClient.invoke('get_asset_upload_token', {
|
|
85
|
+
app_id: appId,
|
|
86
|
+
files: fileDescriptors.map(f => ({ path: f.path, content_type: f.content_type, size: f.size }))
|
|
87
|
+
});
|
|
88
|
+
const token = tokenResponse.message || tokenResponse;
|
|
89
|
+
const { signed_urls, upload_headers, objects, storage } = token;
|
|
90
|
+
|
|
91
|
+
// 2. PUT each file directly to GCS using its signed URL and the server's exact header bag.
|
|
92
|
+
progress(options, chalk.gray(`\n Uploading ${fileDescriptors.length} file(s)...`));
|
|
93
|
+
const uploaded = [];
|
|
94
|
+
const errors = [];
|
|
95
|
+
for (const f of fileDescriptors) {
|
|
96
|
+
const signedUrl = signed_urls?.[f.path];
|
|
97
|
+
if (!signedUrl) {
|
|
98
|
+
errors.push(`No signed URL for: ${f.path}`);
|
|
99
|
+
progress(options, chalk.red(` x ${f.path}`));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Never re-derive Content-Type (or the length-range header) locally for the PUT — the
|
|
103
|
+
// server binds them into the v4 signature. A path the server did not ferry a header bag
|
|
104
|
+
// for is an error for that path, naming the server as the thing that is missing it.
|
|
105
|
+
const headers = upload_headers?.[f.path];
|
|
106
|
+
if (!headers) {
|
|
107
|
+
errors.push(
|
|
108
|
+
`No upload_headers for: ${f.path} (get_asset_upload_token response is missing the header ` +
|
|
109
|
+
`bag for this path)`
|
|
110
|
+
);
|
|
111
|
+
progress(options, chalk.red(` x ${f.path}`));
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const content = await fs.readFile(f._absolutePath);
|
|
116
|
+
const resp = await fetch(signedUrl, {
|
|
117
|
+
method: 'PUT',
|
|
118
|
+
headers,
|
|
119
|
+
body: content
|
|
120
|
+
});
|
|
121
|
+
if (!resp.ok) {
|
|
122
|
+
errors.push(`Failed ${f.path}: ${resp.status} ${resp.statusText}`);
|
|
123
|
+
progress(options, chalk.red(` x ${f.path}`));
|
|
124
|
+
} else {
|
|
125
|
+
const obj = (objects || []).find(o => o.path === f.path) || {};
|
|
126
|
+
uploaded.push({
|
|
127
|
+
path: f.path,
|
|
128
|
+
gcs_path: obj.gcs_path || null,
|
|
129
|
+
public_url: obj.public_url || null,
|
|
130
|
+
content_type: f.content_type,
|
|
131
|
+
size: f.size
|
|
132
|
+
});
|
|
133
|
+
progress(options, chalk.green(` + ${f.path}`));
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
errors.push(`Error ${f.path}: ${err.message}`);
|
|
137
|
+
progress(options, chalk.red(` x ${f.path}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
progress(options, chalk.yellow(`\n ${errors.length} error(s):`));
|
|
143
|
+
errors.forEach(e => progress(options, chalk.red(` - ${e}`)));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (uploaded.length === 0) {
|
|
147
|
+
throw new Error('No files uploaded.');
|
|
148
|
+
}
|
|
149
|
+
if (errors.length > 0) {
|
|
150
|
+
// FAIL LOUD on a partial upload: the files that went up are listed above, but a caller that
|
|
151
|
+
// reads only the exit status must not take a half-uploaded set as success.
|
|
152
|
+
throw new Error(`${errors.length} of ${fileDescriptors.length} file(s) failed to upload:\n - ${errors.join('\n - ')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = { app_id: appId, assets: uploaded, storage: storage || null };
|
|
156
|
+
|
|
157
|
+
if (options.json) {
|
|
158
|
+
console.log('\n' + JSON.stringify(result, null, 2) + '\n');
|
|
159
|
+
} else {
|
|
160
|
+
console.log(chalk.green(`\n ✅ Uploaded ${uploaded.length} asset(s).\n`));
|
|
161
|
+
console.log(chalk.cyan(' Asset references (pass the path to your app handler; the service fetches via get_app_asset):'));
|
|
162
|
+
uploaded.forEach(u => {
|
|
163
|
+
console.log(chalk.white(` ${u.path}`));
|
|
164
|
+
console.log(chalk.gray(` public_url: ${u.public_url}`));
|
|
165
|
+
});
|
|
166
|
+
if (result.storage) {
|
|
167
|
+
// projected_bytes is the app's total INCLUDING this upload; used_bytes is the total before it.
|
|
168
|
+
const usedMb = (result.storage.projected_bytes / (1024 * 1024)).toFixed(1);
|
|
169
|
+
const limitMb = (result.storage.limit_bytes / (1024 * 1024)).toFixed(1);
|
|
170
|
+
console.log(chalk.gray(`\n Storage: ${usedMb} MB / ${limitMb} MB used`));
|
|
171
|
+
}
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolveGatewayCertContext — the CLI-side mirror of which cert `descix serve`
|
|
3
|
+
* will load for a given cwd. ONE owner within the CLI: `doctor.js` and
|
|
4
|
+
* `dev-certs.js` both call this instead of re-deriving workspace lookup +
|
|
5
|
+
* resolveDevCertOptions + resolveCertPaths by hand — the same two-derivations
|
|
6
|
+
* risk `getViteHttpsConfig.js` names for the gateway itself.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors gateway.js exactly: resolveDevCertOptions(workspaceRoot, {}, config)
|
|
9
|
+
* then resolveCertPaths(certOpts) — both from @descix/app-sdk/dev, the actual
|
|
10
|
+
* owner of "which TLS cert a local dev server uses". This module only supplies
|
|
11
|
+
* the workspaceRoot/config a CLI command has to hand; it never re-derives the
|
|
12
|
+
* precedence itself.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
findWorkspaceRoot,
|
|
16
|
+
readWorkspaceConfig,
|
|
17
|
+
resolveDevCertOptions,
|
|
18
|
+
resolveCertPaths,
|
|
19
|
+
resolveGatewayPort,
|
|
20
|
+
} from '@descix/app-sdk/dev';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {string} cwd
|
|
24
|
+
* @returns {{
|
|
25
|
+
* certPath: string,
|
|
26
|
+
* keyPath: string,
|
|
27
|
+
* workspaceRoot: string|null,
|
|
28
|
+
* config: Object|null,
|
|
29
|
+
* port: number,
|
|
30
|
+
* portSource: string,
|
|
31
|
+
* }}
|
|
32
|
+
*/
|
|
33
|
+
export function resolveGatewayCertContext(cwd) {
|
|
34
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
35
|
+
let config = null;
|
|
36
|
+
if (workspaceRoot) {
|
|
37
|
+
try {
|
|
38
|
+
config = readWorkspaceConfig(workspaceRoot);
|
|
39
|
+
} catch {
|
|
40
|
+
// A malformed workspace is the workspace resolver's problem to report
|
|
41
|
+
// elsewhere; this resolver just falls back to "no workspace opinion",
|
|
42
|
+
// same as resolveDevCertOptions does for a missing/malformed file.
|
|
43
|
+
config = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const certOpts = resolveDevCertOptions(workspaceRoot || cwd, {}, config);
|
|
47
|
+
const { certPath, keyPath } = resolveCertPaths(certOpts);
|
|
48
|
+
const { port, portSource } = resolveGatewayPort(config || {}, {});
|
|
49
|
+
return { certPath, keyPath, workspaceRoot, config, port, portSource };
|
|
50
|
+
}
|
package/lib/output.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI output — ONE OWNER of the "human progress vs. --json data channel" split.
|
|
3
|
+
*
|
|
4
|
+
* Relocated from bin/descix.js (WS-MEDIA-QUOTA) so lib/commands/* modules can share it
|
|
5
|
+
* without bin/ -> lib/ layering going backwards. This is a move, not a duplication: bin/descix.js
|
|
6
|
+
* imports `progress` from here instead of defining its own copy.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emit human-facing PROGRESS for a command that can also emit JSON.
|
|
11
|
+
*
|
|
12
|
+
* When `--json` is set, stdout is a DATA CHANNEL: it must contain the JSON document and
|
|
13
|
+
* nothing else, or every scripted consumer breaks. Progress then goes to stderr, where a
|
|
14
|
+
* human piping stdout still sees it. Without `--json`, stdout is the human channel and
|
|
15
|
+
* progress belongs there.
|
|
16
|
+
*
|
|
17
|
+
* ONE OWNER of that decision. Before this existed, each --json command re-decided it by
|
|
18
|
+
* calling console.log directly, and both of them got it wrong: `mcp execute --json` prefixed
|
|
19
|
+
* the document with "Executing <tool>..." and `app media-upload --json` prefixed it with a
|
|
20
|
+
* header, a per-file listing and a per-file +/x line. Both exited 0, so a consumer saw a
|
|
21
|
+
* success code and an unparseable stream. Measured 2026-08-21/22 (seat BEAST): two seats had
|
|
22
|
+
* independently hand-rolled prefix-stripping workarounds (`tail -n +2`, `raw.find('[')`)
|
|
23
|
+
* rather than reporting it.
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options - the command's parsed options (read for .json)
|
|
26
|
+
* @param {...any} args - passed through to console.log / console.error
|
|
27
|
+
*/
|
|
28
|
+
export function progress(options, ...args) {
|
|
29
|
+
if (options && options.json) console.error(...args);
|
|
30
|
+
else console.log(...args);
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"descix-mcp": "bin/mcp-server.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node --test \"tests/*.test.js\"",
|
|
17
|
+
"test": "node --experimental-test-module-mocks --test \"tests/*.test.js\"",
|
|
18
18
|
"pack": "npm pack",
|
|
19
19
|
"check:doc-paths": "node scripts/check-doc-paths.js",
|
|
20
20
|
"briefer": "node scripts/briefer/run.mjs"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "DeSciX",
|
|
31
31
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@descix/app-sdk": "^0.1.
|
|
33
|
+
"@descix/app-sdk": "^0.1.8",
|
|
34
34
|
"@descix/sdk": "^2.0.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
36
36
|
"axios": "^1.7.9",
|
|
@@ -174,6 +174,16 @@ resolves `--port` > `env.gateway.port` > 5173 and is printed with its source, so
|
|
|
174
174
|
never hardcode `:5173`. Set your framework's base path to `/p/<appId>` — the
|
|
175
175
|
gateway does not rewrite paths.
|
|
176
176
|
|
|
177
|
+
**Passkey sign-in needs a trusted dev certificate.** If Powch sign-in under
|
|
178
|
+
`descix serve` fails with `User cancelled`, or the browser console says
|
|
179
|
+
`WebAuthn is not supported on sites with TLS certificate errors`, the gateway's
|
|
180
|
+
certificate is not trusted. Run `descix dev-certs check` (or `descix doctor`, row
|
|
181
|
+
"Dev certificate"). If it is not trusted, do not try to trust it yourself: give
|
|
182
|
+
the user exactly `descix dev-certs trust` to run (macOS asks for their password),
|
|
183
|
+
tell them to quit and reopen Chrome, then run `descix dev-certs check` again. On
|
|
184
|
+
Linux and Windows the check reports `unverifiable` and trusting is a manual,
|
|
185
|
+
unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
|
|
186
|
+
|
|
177
187
|
## Integrating Existing Content
|
|
178
188
|
|
|
179
189
|
If the repo already has content (an HTML page, a React app, an SDK with docs),
|
package/templates/agent-cline.md
CHANGED
|
@@ -136,6 +136,16 @@ resolves `--port` > `env.gateway.port` > 5173 and is printed with its source, so
|
|
|
136
136
|
never hardcode `:5173`. Set your framework's base path to `/p/<appId>` — the
|
|
137
137
|
gateway does not rewrite paths.
|
|
138
138
|
|
|
139
|
+
**Passkey sign-in needs a trusted dev certificate.** If Powch sign-in under
|
|
140
|
+
`descix serve` fails with `User cancelled`, or the browser console says
|
|
141
|
+
`WebAuthn is not supported on sites with TLS certificate errors`, the gateway's
|
|
142
|
+
certificate is not trusted. Run `descix dev-certs check` (or `descix doctor`, row
|
|
143
|
+
"Dev certificate"). If it is not trusted, do not try to trust it yourself: give
|
|
144
|
+
the user exactly `descix dev-certs trust` to run (macOS asks for their password),
|
|
145
|
+
tell them to quit and reopen Chrome, then run `descix dev-certs check` again. On
|
|
146
|
+
Linux and Windows the check reports `unverifiable` and trusting is a manual,
|
|
147
|
+
unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
|
|
148
|
+
|
|
139
149
|
## MCP Tools
|
|
140
150
|
|
|
141
151
|
- `descix_doctor` — Startup diagnostic (call first)
|
|
@@ -136,6 +136,16 @@ resolves `--port` > `env.gateway.port` > 5173 and is printed with its source, so
|
|
|
136
136
|
never hardcode `:5173`. Set your framework's base path to `/p/<appId>` — the
|
|
137
137
|
gateway does not rewrite paths.
|
|
138
138
|
|
|
139
|
+
**Passkey sign-in needs a trusted dev certificate.** If Powch sign-in under
|
|
140
|
+
`descix serve` fails with `User cancelled`, or the browser console says
|
|
141
|
+
`WebAuthn is not supported on sites with TLS certificate errors`, the gateway's
|
|
142
|
+
certificate is not trusted. Run `descix dev-certs check` (or `descix doctor`, row
|
|
143
|
+
"Dev certificate"). If it is not trusted, do not try to trust it yourself: give
|
|
144
|
+
the user exactly `descix dev-certs trust` to run (macOS asks for their password),
|
|
145
|
+
tell them to quit and reopen Chrome, then run `descix dev-certs check` again. On
|
|
146
|
+
Linux and Windows the check reports `unverifiable` and trusting is a manual,
|
|
147
|
+
unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
|
|
148
|
+
|
|
139
149
|
## MCP Tools
|
|
140
150
|
|
|
141
151
|
- `descix_doctor` — Startup diagnostic (call first)
|
|
@@ -136,6 +136,16 @@ resolves `--port` > `env.gateway.port` > 5173 and is printed with its source, so
|
|
|
136
136
|
never hardcode `:5173`. Set your framework's base path to `/p/<appId>` — the
|
|
137
137
|
gateway does not rewrite paths.
|
|
138
138
|
|
|
139
|
+
**Passkey sign-in needs a trusted dev certificate.** If Powch sign-in under
|
|
140
|
+
`descix serve` fails with `User cancelled`, or the browser console says
|
|
141
|
+
`WebAuthn is not supported on sites with TLS certificate errors`, the gateway's
|
|
142
|
+
certificate is not trusted. Run `descix dev-certs check` (or `descix doctor`, row
|
|
143
|
+
"Dev certificate"). If it is not trusted, do not try to trust it yourself: give
|
|
144
|
+
the user exactly `descix dev-certs trust` to run (macOS asks for their password),
|
|
145
|
+
tell them to quit and reopen Chrome, then run `descix dev-certs check` again. On
|
|
146
|
+
Linux and Windows the check reports `unverifiable` and trusting is a manual,
|
|
147
|
+
unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
|
|
148
|
+
|
|
139
149
|
## MCP Tools
|
|
140
150
|
|
|
141
151
|
- `descix_doctor` — Startup diagnostic (call first)
|