@fluid-app/fluid-cli-mist 0.1.12 → 0.1.14
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/index.mjs +143 -66
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -579,10 +579,10 @@ async function ensureBotIdentity(cwd) {
|
|
|
579
579
|
} catch {}
|
|
580
580
|
}
|
|
581
581
|
/**
|
|
582
|
-
* Append
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
582
|
+
* Append Mist's local-only state to `.git/info/exclude` when needed:
|
|
583
|
+
* `.mist` is the clone marker from spec §3, while `.mist-desktop/`
|
|
584
|
+
* contains private chat history, attachments, screenshots, and other
|
|
585
|
+
* machine-local Desktop state. Neither belongs in the project remote.
|
|
586
586
|
*
|
|
587
587
|
* Why local exclude and not `.gitignore`: `.gitignore` lives in the
|
|
588
588
|
* template repo and would need a template release to reach existing
|
|
@@ -593,9 +593,8 @@ async function ensureBotIdentity(cwd) {
|
|
|
593
593
|
*
|
|
594
594
|
* Idempotent and best-effort: never throws. A repo with an unusual
|
|
595
595
|
* `.git` layout (missing `info/`, `.git` as a worktree pointer file,
|
|
596
|
-
* etc.) just skips the exclude — `gitAutoCommit`
|
|
597
|
-
*
|
|
598
|
-
* the commit regardless.
|
|
596
|
+
* etc.) just skips the exclude — `gitAutoCommit` also cleans both paths
|
|
597
|
+
* from the index after staging.
|
|
599
598
|
*/
|
|
600
599
|
function excludeMistMarkerFromGit(cwd) {
|
|
601
600
|
try {
|
|
@@ -605,8 +604,10 @@ function excludeMistMarkerFromGit(cwd) {
|
|
|
605
604
|
if (!existsSync(infoDir)) mkdirSync(infoDir, { recursive: true });
|
|
606
605
|
const excludePath = path.join(infoDir, "exclude");
|
|
607
606
|
const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
608
|
-
|
|
609
|
-
|
|
607
|
+
const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter(Boolean));
|
|
608
|
+
const missing = [".mist", ".mist-desktop/"].filter((entry) => !existingLines.has(entry));
|
|
609
|
+
if (missing.length === 0) return;
|
|
610
|
+
writeFileSync(excludePath, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
|
|
610
611
|
} catch {}
|
|
611
612
|
}
|
|
612
613
|
/**
|
|
@@ -626,10 +627,9 @@ function excludeMistMarkerFromGit(cwd) {
|
|
|
626
627
|
* the repo landed on disk (fresh clone, pre-existing clone, or terminal CLI).
|
|
627
628
|
* The real person is still recorded in the commit *message* (see push.ts).
|
|
628
629
|
*
|
|
629
|
-
*
|
|
630
|
-
* `.git/info/exclude
|
|
631
|
-
*
|
|
632
|
-
* silently failed. Belt AND suspenders:
|
|
630
|
+
* Local Mist state handling: clone writes `.mist` and `.mist-desktop/` to
|
|
631
|
+
* `.git/info/exclude`, but this heals a pre-existing clone or a repo whose
|
|
632
|
+
* exclude write silently failed. Belt AND suspenders:
|
|
633
633
|
* 1. Re-run the exclude write here (idempotent) — heals pre-existing
|
|
634
634
|
* clones on their very next push. Anchored to the repository root
|
|
635
635
|
* (`git rev-parse --show-toplevel`), not cwd, so a push run from a
|
|
@@ -647,6 +647,10 @@ function excludeMistMarkerFromGit(cwd) {
|
|
|
647
647
|
* case (exclude already written) into a hard failure. Add-then-
|
|
648
648
|
* reset avoids that entirely and is a no-op when there's nothing
|
|
649
649
|
* to unstage.
|
|
650
|
+
* 3. Run `git rm --cached -r --ignore-unmatch .mist-desktop` from the
|
|
651
|
+
* repository root. This keeps every local file on disk while staging
|
|
652
|
+
* their removal if an older publish already tracked them. Ignore rules
|
|
653
|
+
* alone cannot untrack an existing path.
|
|
650
654
|
* Note: a `.mist` file already committed in an existing repo's history
|
|
651
655
|
* is left alone — this only prevents *new* adds, it does not untrack
|
|
652
656
|
* anything or rewrite history.
|
|
@@ -675,6 +679,17 @@ async function gitAutoCommit(cwd, message) {
|
|
|
675
679
|
".mist"
|
|
676
680
|
], { cwd });
|
|
677
681
|
} catch {}
|
|
682
|
+
try {
|
|
683
|
+
await execa("git", [
|
|
684
|
+
"rm",
|
|
685
|
+
"-q",
|
|
686
|
+
"-r",
|
|
687
|
+
"--cached",
|
|
688
|
+
"--ignore-unmatch",
|
|
689
|
+
"--",
|
|
690
|
+
".mist-desktop"
|
|
691
|
+
], { cwd: repoRoot });
|
|
692
|
+
} catch {}
|
|
678
693
|
const identityArgs = [
|
|
679
694
|
"-c",
|
|
680
695
|
`user.name=${MIST_GIT_AUTHOR_NAME}`,
|
|
@@ -846,6 +861,12 @@ const STATE_COLOR$1 = {
|
|
|
846
861
|
function colorState(state) {
|
|
847
862
|
return STATE_COLOR$1[state](state);
|
|
848
863
|
}
|
|
864
|
+
/** Completion copy for `mist push --watch`. Deployment records expose an
|
|
865
|
+
* ephemeral provider URL that may require provider SSO; only the Mist's
|
|
866
|
+
* stable public URL is suitable to hand to a user. */
|
|
867
|
+
function readyDeploymentMessage(publicUrl, slug) {
|
|
868
|
+
return publicUrl ? `✔ Ready: ${chalk.cyan(publicUrl)}` : `✔ Deployment ready. The public URL is still provisioning; run \`fluid mist show ${slug}\` to check it.`;
|
|
869
|
+
}
|
|
849
870
|
/**
|
|
850
871
|
* Bold display label for a mist, falling back to a dim "(unnamed)"
|
|
851
872
|
* placeholder when both `name` and `vendorName` are missing. Never nests
|
|
@@ -1056,7 +1077,41 @@ function createSilentReporter() {
|
|
|
1056
1077
|
};
|
|
1057
1078
|
}
|
|
1058
1079
|
//#endregion
|
|
1080
|
+
//#region ../../mist/core/src/lifecycle-urls.ts
|
|
1081
|
+
function normalizeLifecycleOrigin(value) {
|
|
1082
|
+
let url;
|
|
1083
|
+
try {
|
|
1084
|
+
url = new URL(value);
|
|
1085
|
+
} catch {
|
|
1086
|
+
throw new Error("Lifecycle origin must be an absolute HTTP(S) URL");
|
|
1087
|
+
}
|
|
1088
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("Lifecycle origin must be an absolute HTTP(S) URL");
|
|
1089
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
1090
|
+
url.search = "";
|
|
1091
|
+
url.hash = "";
|
|
1092
|
+
return url.toString().replace(/\/$/, "");
|
|
1093
|
+
}
|
|
1094
|
+
function lifecycleUrls(publicUrl, paths = {}) {
|
|
1095
|
+
const origin = normalizeLifecycleOrigin(publicUrl);
|
|
1096
|
+
return {
|
|
1097
|
+
installWebhookUrl: `${origin}${normalizePath(paths.installPath ?? "/api/webhooks/installed")}`,
|
|
1098
|
+
uninstallWebhookUrl: `${origin}${normalizePath(paths.uninstallPath ?? "/api/webhooks/uninstalled")}`
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
function normalizePath(value) {
|
|
1102
|
+
return `/${value.replace(/^\/+/, "")}`;
|
|
1103
|
+
}
|
|
1104
|
+
//#endregion
|
|
1059
1105
|
//#region src/api/droplets.ts
|
|
1106
|
+
/**
|
|
1107
|
+
* Droplet CRUD over `/api/droplets`. Thin wrapper — the provisioning
|
|
1108
|
+
* orchestration (idempotency, embed-url readiness, attach) lives in
|
|
1109
|
+
* `lib/droplet-provision.ts`.
|
|
1110
|
+
*
|
|
1111
|
+
* Note: droplets are a company-level resource, so unlike the mist
|
|
1112
|
+
* sub-resources these paths take the droplet `uuid` (drp_…) directly and
|
|
1113
|
+
* don't go through `resolveMistId`.
|
|
1114
|
+
*/
|
|
1060
1115
|
const BASE$3 = "/api/droplets";
|
|
1061
1116
|
/**
|
|
1062
1117
|
* Full scope set from `Droplet::Installation::VALID_SCOPES`. We request
|
|
@@ -1077,12 +1132,14 @@ const DEFAULT_DROPLET_SCOPES = [
|
|
|
1077
1132
|
"header_menus",
|
|
1078
1133
|
"payments"
|
|
1079
1134
|
];
|
|
1080
|
-
/** Lifecycle webhook paths a Mist Next app serves (mirrors Mist Desktop).
|
|
1135
|
+
/** Lifecycle webhook paths a Mist Next app serves (mirrors Mist Desktop).
|
|
1136
|
+
* `embedUrl` must be an absolute HTTP(S) URL so malformed deployment metadata
|
|
1137
|
+
* fails before a Droplet is created with unusable callback URLs. */
|
|
1081
1138
|
function dropletWebhookUrls(embedUrl) {
|
|
1082
|
-
const
|
|
1139
|
+
const urls = lifecycleUrls(embedUrl);
|
|
1083
1140
|
return {
|
|
1084
|
-
install_webhook_url:
|
|
1085
|
-
uninstall_webhook_url:
|
|
1141
|
+
install_webhook_url: urls.installWebhookUrl,
|
|
1142
|
+
uninstall_webhook_url: urls.uninstallWebhookUrl
|
|
1086
1143
|
};
|
|
1087
1144
|
}
|
|
1088
1145
|
/**
|
|
@@ -1131,16 +1188,22 @@ async function findDropletUuidByNumericId(client, id) {
|
|
|
1131
1188
|
}
|
|
1132
1189
|
return null;
|
|
1133
1190
|
}
|
|
1134
|
-
/**
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
function
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1191
|
+
/**
|
|
1192
|
+
* Recover a Droplet created by an interrupted surface-provisioning run. The
|
|
1193
|
+
* canonical Mist embed URL is stable and unique to that deployment, so a
|
|
1194
|
+
* matching unlinked Droplet can be attached instead of creating a duplicate.
|
|
1195
|
+
*/
|
|
1196
|
+
async function findDropletByEmbedUrl(client, embedUrl) {
|
|
1197
|
+
const expectedUrl = embedUrl.replace(/\/$/, "");
|
|
1198
|
+
for (let page = 1; page <= MAX_PAGES$1; page++) {
|
|
1199
|
+
const resp = await client.get(`${BASE$3}?page=${page}&per_page=${PER_PAGE$1}`);
|
|
1200
|
+
const batch = resp.droplets ?? [];
|
|
1201
|
+
const match = batch.find((droplet) => droplet.embed_url?.replace(/\/$/, "") === expectedUrl);
|
|
1202
|
+
if (match) return match;
|
|
1203
|
+
const totalPages = resp.meta?.pagination?.total_pages;
|
|
1204
|
+
if (totalPages != null ? page >= totalPages : batch.length < PER_PAGE$1) break;
|
|
1205
|
+
}
|
|
1206
|
+
return null;
|
|
1144
1207
|
}
|
|
1145
1208
|
//#endregion
|
|
1146
1209
|
//#region src/api/droplet_installations.ts
|
|
@@ -1358,37 +1421,35 @@ async function provisionSurface(client, options) {
|
|
|
1358
1421
|
timing,
|
|
1359
1422
|
sleep
|
|
1360
1423
|
});
|
|
1424
|
+
if (!embedUrl) throw new Error("The mist has no public_url yet — wait for the first deploy before creating a surface.");
|
|
1361
1425
|
const name = options.name?.trim() || mist.name?.trim() || mist.vendor_name || mist.slug;
|
|
1362
|
-
reporter.start(`Creating ${label} "${name}"…`);
|
|
1363
1426
|
if (kind === "droplet") {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
reporter.succeed("Embed URL set");
|
|
1388
|
-
} else reporter.warn("Mist public_url not ready yet — re-run once the first deploy finishes to set the embed URL");
|
|
1427
|
+
reporter.start("Checking for an interrupted Droplet create…");
|
|
1428
|
+
const recovered = await findDropletByEmbedUrl(client, embedUrl);
|
|
1429
|
+
if (recovered) {
|
|
1430
|
+
surfaceUuid = recovered.uuid;
|
|
1431
|
+
surfaceNumericId = recovered.id ?? null;
|
|
1432
|
+
surfaceName = recovered.name;
|
|
1433
|
+
attachUuid = recovered.uuid;
|
|
1434
|
+
embedUrlSet = true;
|
|
1435
|
+
reporter.skip(`Reusing droplet ${surfaceUuid} from an interrupted attach`);
|
|
1436
|
+
} else {
|
|
1437
|
+
reporter.start(`Creating ${label} "${name}"…`);
|
|
1438
|
+
const created = await createDroplet(client, { droplet: buildDropletCreateInput({
|
|
1439
|
+
name,
|
|
1440
|
+
embedUrl,
|
|
1441
|
+
requestedScopes: options.requestedScopes
|
|
1442
|
+
}) });
|
|
1443
|
+
surfaceUuid = created.droplet.uuid;
|
|
1444
|
+
surfaceNumericId = created.droplet.id ?? null;
|
|
1445
|
+
surfaceName = created.droplet.name;
|
|
1446
|
+
attachUuid = created.droplet.uuid;
|
|
1447
|
+
embedUrlSet = true;
|
|
1448
|
+
surfaceCreated = true;
|
|
1449
|
+
reporter.succeed(`Created droplet ${surfaceUuid}`);
|
|
1389
1450
|
}
|
|
1390
1451
|
} else if (kind === "drop-zone") {
|
|
1391
|
-
|
|
1452
|
+
reporter.start(`Creating ${label} "${name}"…`);
|
|
1392
1453
|
const created = await createDropZone(client, { drop_zone: buildDropZoneCreateInput({
|
|
1393
1454
|
name,
|
|
1394
1455
|
embedUrl,
|
|
@@ -1399,9 +1460,10 @@ async function provisionSurface(client, options) {
|
|
|
1399
1460
|
surfaceName = created.drop_zone.name;
|
|
1400
1461
|
attachUuid = created.drop_zone.uuid;
|
|
1401
1462
|
embedUrlSet = true;
|
|
1463
|
+
surfaceCreated = true;
|
|
1402
1464
|
reporter.succeed(`Created drop zone ${surfaceUuid}`);
|
|
1403
1465
|
} else {
|
|
1404
|
-
|
|
1466
|
+
reporter.start(`Creating ${label} "${name}"…`);
|
|
1405
1467
|
const created = await createMobileWidget(client, { mobile_widget: buildMobileWidgetCreateInput({
|
|
1406
1468
|
name,
|
|
1407
1469
|
embedUrl,
|
|
@@ -1412,9 +1474,9 @@ async function provisionSurface(client, options) {
|
|
|
1412
1474
|
surfaceName = created.mobile_widget.name;
|
|
1413
1475
|
attachId = created.mobile_widget.id;
|
|
1414
1476
|
embedUrlSet = true;
|
|
1477
|
+
surfaceCreated = true;
|
|
1415
1478
|
reporter.succeed(`Created mobile embed #${surfaceNumericId}`);
|
|
1416
1479
|
}
|
|
1417
|
-
surfaceCreated = true;
|
|
1418
1480
|
} else {
|
|
1419
1481
|
surfaceUuid = options.existingUuid ?? null;
|
|
1420
1482
|
surfaceNumericId = options.existingId ?? null;
|
|
@@ -1523,7 +1585,7 @@ async function ensureDropletInstalled(client, args) {
|
|
|
1523
1585
|
created: true
|
|
1524
1586
|
};
|
|
1525
1587
|
} catch (err) {
|
|
1526
|
-
if (
|
|
1588
|
+
if (err instanceof MistApiError && (err.status === 409 || err.status === 422)) {
|
|
1527
1589
|
const confirmed = await lookupInstallation(client, dropletUuid);
|
|
1528
1590
|
if (confirmed) {
|
|
1529
1591
|
reporter.skip("Droplet already installed — skipping");
|
|
@@ -1537,9 +1599,6 @@ async function ensureDropletInstalled(client, args) {
|
|
|
1537
1599
|
throw normalizeInstallError(err, dropletUuid);
|
|
1538
1600
|
}
|
|
1539
1601
|
}
|
|
1540
|
-
function isAlreadyInstalledError(err) {
|
|
1541
|
-
return err instanceof MistApiError && (err.status === 409 || err.status === 422) && /already|taken|installed|exists/i.test(`${err.message} ${JSON.stringify(err.details ?? {})}`);
|
|
1542
|
-
}
|
|
1543
1602
|
/** Best-effort installation lookup for a droplet; null when the row can't
|
|
1544
1603
|
* be found or the list call fails. */
|
|
1545
1604
|
async function lookupInstallation(client, dropletUuid) {
|
|
@@ -2038,10 +2097,21 @@ async function fetchCurrentUser(client) {
|
|
|
2038
2097
|
const POLL_MS$1 = 2e3;
|
|
2039
2098
|
const LOCATE_TIMEOUT_MS = 2 * 6e4;
|
|
2040
2099
|
const READY_TIMEOUT_MS = 5 * 6e4;
|
|
2100
|
+
const PUBLIC_URL_TIMEOUT_MS = 5e3;
|
|
2041
2101
|
function sleep$1(ms) {
|
|
2042
2102
|
return new Promise((r) => setTimeout(r, ms));
|
|
2043
2103
|
}
|
|
2044
|
-
|
|
2104
|
+
async function withTimeout(promise, timeoutMs) {
|
|
2105
|
+
let timer = null;
|
|
2106
|
+
try {
|
|
2107
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
2108
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`timed out after ${timeoutMs / 1e3}s`)), timeoutMs);
|
|
2109
|
+
})]);
|
|
2110
|
+
} finally {
|
|
2111
|
+
if (timer) clearTimeout(timer);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
const pushCommand = new Command("push").description("Commit working-tree changes and push to the Mist-managed remote (triggers a hosted deployment)").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--watch", "Poll /deployments until ready or error").option("--branch <name>", "Branch to push (default: main)").option("--no-commit", "Skip the auto-commit step (push existing commits only)").option("-m, --message <message>", "Commit message for the auto-commit (default: timestamped Publish)").action((slug, opts) => runAction(async () => {
|
|
2045
2115
|
await ensureGitAvailable();
|
|
2046
2116
|
const resolved = resolveSlug(slug);
|
|
2047
2117
|
const client = createMistClient();
|
|
@@ -2078,7 +2148,7 @@ const pushCommand = new Command("push").description("Commit working-tree changes
|
|
|
2078
2148
|
let namePrefix = "";
|
|
2079
2149
|
let idSuffix = "";
|
|
2080
2150
|
try {
|
|
2081
|
-
const me = await
|
|
2151
|
+
const me = await withTimeout(fetchCurrentUser(client), 5e3);
|
|
2082
2152
|
const name = me.full_name && me.full_name.trim() || me.email || (Number.isFinite(me.id) ? `user-${me.id}` : null);
|
|
2083
2153
|
const id = me.public_id && me.public_id.trim();
|
|
2084
2154
|
if (name) namePrefix = `${name}: `;
|
|
@@ -2131,9 +2201,9 @@ const pushCommand = new Command("push").description("Commit working-tree changes
|
|
|
2131
2201
|
await sleep$1(POLL_MS$1);
|
|
2132
2202
|
}
|
|
2133
2203
|
if (!deploymentId) {
|
|
2134
|
-
console.
|
|
2135
|
-
console.
|
|
2136
|
-
|
|
2204
|
+
console.warn(chalk.yellow(`Push succeeded, but hosting has not started commit ${pushedSha?.slice(0, 7) ?? "(unknown)"} after ${LOCATE_TIMEOUT_MS / 1e3}s.`));
|
|
2205
|
+
console.warn(chalk.yellow("The deployment is still queued and may appear later. Run `fluid mist deployments` to check it — do not retry or rewrite the commit just because the hosting queue is slow."));
|
|
2206
|
+
return;
|
|
2137
2207
|
}
|
|
2138
2208
|
const readyStart = Date.now();
|
|
2139
2209
|
let lastState = null;
|
|
@@ -2144,7 +2214,14 @@ const pushCommand = new Command("push").description("Commit working-tree changes
|
|
|
2144
2214
|
lastState = deployment.state;
|
|
2145
2215
|
}
|
|
2146
2216
|
if (deployment.state === "ready") {
|
|
2147
|
-
|
|
2217
|
+
let publicUrl = null;
|
|
2218
|
+
try {
|
|
2219
|
+
publicUrl = (await withTimeout(showMist(client, resolved), PUBLIC_URL_TIMEOUT_MS)).mist.public_url;
|
|
2220
|
+
} catch (err) {
|
|
2221
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2222
|
+
console.warn(chalk.yellow(` Deployment is ready, but the public URL lookup failed: ${reason}`));
|
|
2223
|
+
}
|
|
2224
|
+
console.log(readyDeploymentMessage(publicUrl, resolved));
|
|
2148
2225
|
return;
|
|
2149
2226
|
}
|
|
2150
2227
|
if (deployment.state === "error" || deployment.state === "canceled") {
|