@dashai/cli 0.12.1 → 0.13.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 CHANGED
@@ -428,6 +428,25 @@ async function getModuleBySlug(slug, options = {}) {
428
428
  options
429
429
  );
430
430
  }
431
+ function isInstallGated(r) {
432
+ return r.install_gated === true;
433
+ }
434
+ async function installModule(moduleId, body, options = {}) {
435
+ return apiRequest(
436
+ "POST",
437
+ `/api/modules/${moduleId}/install`,
438
+ body,
439
+ options
440
+ );
441
+ }
442
+ async function listWorkspaceInstallations(workspaceId, options = {}) {
443
+ return apiRequest(
444
+ "GET",
445
+ `/api/modules/workspaces/${workspaceId}/installations`,
446
+ void 0,
447
+ options
448
+ );
449
+ }
431
450
  async function applyDraftManifest(moduleId, manifest, options = {}) {
432
451
  return apiRequest(
433
452
  "PUT",
@@ -5200,6 +5219,386 @@ async function moduleDiffCommand(options) {
5200
5219
  // src/bin.ts
5201
5220
  init_init();
5202
5221
 
5222
+ // src/commands/module/install.ts
5223
+ init_api_client();
5224
+ init_config();
5225
+ init_manifest_io();
5226
+
5227
+ // src/lib/module-lifecycle.ts
5228
+ init_api_client();
5229
+ init_output();
5230
+ init_workspaces();
5231
+ function isDevInstallKind(kind) {
5232
+ return kind === "local" || kind === "sandbox";
5233
+ }
5234
+ function isRealRuntimeUrl(url) {
5235
+ return !!url && /^https:\/\//i.test(url);
5236
+ }
5237
+ function isDraftVersion(v) {
5238
+ return !v || v.includes("draft");
5239
+ }
5240
+ function compareSemver(a, b) {
5241
+ const parse = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
5242
+ const pa = parse(a);
5243
+ const pb = parse(b);
5244
+ for (let i = 0; i < 3; i++) {
5245
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
5246
+ if (d !== 0) return d > 0 ? 1 : -1;
5247
+ }
5248
+ return 0;
5249
+ }
5250
+ function isUpdateAvailable(installedVersion, latestVersion) {
5251
+ return !!installedVersion && !!latestVersion && !isDraftVersion(installedVersion) && !isDraftVersion(latestVersion) && compareSemver(latestVersion, installedVersion) > 0;
5252
+ }
5253
+ async function resolveWorkspaceWithFallback(opts) {
5254
+ const fromCache = opts.userFlag === void 0 && opts.cachedWorkspaceId !== void 0;
5255
+ const flag = opts.userFlag ?? (fromCache ? String(opts.cachedWorkspaceId) : opts.configDefault);
5256
+ try {
5257
+ return await selectWorkspace({
5258
+ apiUrl: opts.apiUrl,
5259
+ apiToken: opts.apiToken,
5260
+ ...flag !== void 0 ? { flag } : {},
5261
+ interactive: true
5262
+ });
5263
+ } catch (err) {
5264
+ if (!fromCache || err instanceof ApiError) throw err;
5265
+ info(
5266
+ `Cached dev-install workspace ${code(String(opts.cachedWorkspaceId))} is no longer accessible \u2014 pick a workspace.`
5267
+ );
5268
+ return await selectWorkspace({
5269
+ apiUrl: opts.apiUrl,
5270
+ apiToken: opts.apiToken,
5271
+ ...opts.configDefault !== void 0 ? { flag: opts.configDefault } : {},
5272
+ interactive: true
5273
+ });
5274
+ }
5275
+ }
5276
+
5277
+ // src/commands/module/install.ts
5278
+ init_output();
5279
+ async function moduleInstallCommand(slugArg, options) {
5280
+ const slug = resolveSlug(slugArg, options.dir);
5281
+ if (!slug) {
5282
+ fail(
5283
+ `No module slug \u2014 pass one (${code("dashwise module install <slug>")}) or run from a directory with a valid module.json.`
5284
+ );
5285
+ return 1;
5286
+ }
5287
+ if (!isLoggedIn()) {
5288
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
5289
+ return 1;
5290
+ }
5291
+ const cfg = readConfig();
5292
+ if (!cfg) {
5293
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
5294
+ return 1;
5295
+ }
5296
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
5297
+ let mod;
5298
+ try {
5299
+ mod = await getModuleBySlug(slug, auth);
5300
+ } catch (err) {
5301
+ if (err instanceof ApiError && err.status === 404) {
5302
+ fail(
5303
+ `Module ${code(slug)} not found on the backend. Run ${code("dashwise module register")} + ${code("dashwise module publish")} first.`
5304
+ );
5305
+ return 1;
5306
+ }
5307
+ fail(`Could not resolve module ${code(slug)}: ${err.message}`);
5308
+ return 1;
5309
+ }
5310
+ if (!mod.current_version) {
5311
+ fail(
5312
+ `Module ${code(slug)} has no published version yet \u2014 nothing to install. Run ${code("dashwise module publish")} first.`
5313
+ );
5314
+ return 1;
5315
+ }
5316
+ const targetVersion = options.version ?? mod.current_version;
5317
+ let workspace2;
5318
+ try {
5319
+ workspace2 = await resolveWorkspaceWithFallback({
5320
+ userFlag: options.workspace,
5321
+ cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
5322
+ configDefault: cfg.workspaceSlug,
5323
+ apiUrl: resolveApiUrl(),
5324
+ apiToken: cfg.token
5325
+ });
5326
+ } catch (err) {
5327
+ fail(`Workspace selection failed: ${err.message}`);
5328
+ return 1;
5329
+ }
5330
+ let existing;
5331
+ try {
5332
+ const installs = await listWorkspaceInstallations(workspace2.id, auth);
5333
+ existing = installs.find((row2) => row2.module_id === mod.id);
5334
+ } catch (err) {
5335
+ fail(
5336
+ `Could not read workspace installations: ${err.message}`
5337
+ );
5338
+ return 1;
5339
+ }
5340
+ let isUpgrade = false;
5341
+ let devInstallToReplace;
5342
+ let confirmedViaPicker = false;
5343
+ if (existing && isDevInstallKind(existing.kind)) {
5344
+ const resolution = await resolveDevInstallCollision(
5345
+ existing,
5346
+ slug,
5347
+ workspace2.slug,
5348
+ options
5349
+ );
5350
+ if (typeof resolution === "number") return resolution;
5351
+ devInstallToReplace = existing;
5352
+ confirmedViaPicker = resolution === "replace-confirmed";
5353
+ existing = void 0;
5354
+ } else if (existing) {
5355
+ isUpgrade = true;
5356
+ const installedVersion2 = existing.module_version?.version;
5357
+ if (installedVersion2 && installedVersion2 === targetVersion) {
5358
+ info(
5359
+ `Already deployed at ${code(`v${installedVersion2}`)} \u2014 re-running is idempotent and repairs a failed provision.`
5360
+ );
5361
+ } else {
5362
+ info(
5363
+ `Existing production install #${existing.id}${installedVersion2 ? ` at ${code(`v${installedVersion2}`)}` : ""} \u2014 this will upgrade it to ${code(`v${targetVersion}`)}.`
5364
+ );
5365
+ }
5366
+ }
5367
+ if (!options.yes && process.stdout.isTTY && !confirmedViaPicker) {
5368
+ const message = devInstallToReplace ? `Replace the dev install (revokes local-dev credentials) and install ${code(mod.slug)} ${code(`v${targetVersion}`)} into ${code(workspace2.slug)} as production?` : `${isUpgrade ? "Upgrade" : "Install"} ${code(mod.slug)} ${code(`v${targetVersion}`)} into workspace ${code(workspace2.slug)}?`;
5369
+ const answer = await select({
5370
+ message,
5371
+ options: [
5372
+ {
5373
+ value: "go",
5374
+ label: devInstallToReplace ? "Yes, replace the dev install + production-install" : isUpgrade ? "Yes, upgrade now" : "Yes, install now"
5375
+ },
5376
+ { value: "cancel", label: "No, abort" }
5377
+ ],
5378
+ initialValue: devInstallToReplace ? "cancel" : "go"
5379
+ });
5380
+ if (isCancel(answer) || answer === "cancel") {
5381
+ fail("Aborted. Nothing was changed.");
5382
+ return 130;
5383
+ }
5384
+ }
5385
+ let devInstallReplaced = false;
5386
+ if (devInstallToReplace) {
5387
+ const destroyed = await replaceDevInstall(devInstallToReplace, slug, auth);
5388
+ if (destroyed !== 0) return destroyed;
5389
+ devInstallReplaced = true;
5390
+ }
5391
+ info("Provisioning the runtime \u2014 this can take a minute\u2026");
5392
+ let result;
5393
+ try {
5394
+ result = await installModule(
5395
+ mod.id,
5396
+ {
5397
+ workspaceId: workspace2.id,
5398
+ ...options.version !== void 0 ? { version: options.version } : {},
5399
+ ...options.force !== void 0 ? { force: options.force } : {}
5400
+ },
5401
+ auth
5402
+ );
5403
+ } catch (err) {
5404
+ return surfaceInstallError(err, mod.slug, workspace2.slug, devInstallReplaced);
5405
+ }
5406
+ if (isInstallGated(result)) {
5407
+ if (options.json) {
5408
+ out(JSON.stringify(result));
5409
+ return 0;
5410
+ }
5411
+ success(
5412
+ `Install request ${code(`#${result.install_request.id}`)} queued (status: ${result.install_request.status}).`
5413
+ );
5414
+ info(
5415
+ `This module requires author approval before it installs. Once approved, check with ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)}.`
5416
+ );
5417
+ return 0;
5418
+ }
5419
+ let row;
5420
+ let verified = false;
5421
+ try {
5422
+ const after = await listWorkspaceInstallations(workspace2.id, auth);
5423
+ row = after.find((r) => r.module_id === mod.id);
5424
+ verified = row !== void 0;
5425
+ } catch {
5426
+ }
5427
+ const publicUrl = row?.public_url ?? null;
5428
+ const installedVersion = row?.module_version?.version ?? targetVersion;
5429
+ const runtimeProvisioned = isRealRuntimeUrl(row?.runtime_url);
5430
+ if (options.json) {
5431
+ out(
5432
+ JSON.stringify({
5433
+ installation_id: result.installation.id,
5434
+ kind: row?.kind ?? null,
5435
+ version: installedVersion,
5436
+ was_upgrade: result.was_upgrade,
5437
+ public_url: publicUrl,
5438
+ verified
5439
+ })
5440
+ );
5441
+ return 0;
5442
+ }
5443
+ if (!verified) {
5444
+ success(
5445
+ `Installed ${code(mod.slug)} ${code(`v${targetVersion}`)} (install ${code(`#${result.installation.id}`)}).`
5446
+ );
5447
+ info(
5448
+ `The verification read failed \u2014 run ${code(`dashwise module status ${mod.slug} --workspace ${workspace2.slug}`)} to see the deploy state and public URL.`
5449
+ );
5450
+ return 0;
5451
+ }
5452
+ if (runtimeProvisioned && publicUrl) {
5453
+ success(
5454
+ `Deployed ${code(mod.slug)} ${code(`v${installedVersion}`)} \u2014 live at ${code(publicUrl)}`
5455
+ );
5456
+ info(
5457
+ `Install ${code(`#${result.installation.id}`)} (kind=${row?.kind ?? "unknown"}) in workspace ${code(workspace2.slug)}${result.was_upgrade ? " \u2014 upgraded in place." : "."}`
5458
+ );
5459
+ } else if (runtimeProvisioned) {
5460
+ success(
5461
+ `Installed ${code(mod.slug)} ${code(`v${installedVersion}`)} (install ${code(`#${result.installation.id}`)}, kind=${row?.kind ?? "unknown"}).`
5462
+ );
5463
+ warn(
5464
+ `The runtime is provisioned, but this platform exposes no public host \u2014 there is no public URL to print. Ask the operator to configure the public module host; re-running install won't change this.`
5465
+ );
5466
+ } else {
5467
+ success(
5468
+ `Installed ${code(mod.slug)} ${code(`v${installedVersion}`)} (install ${code(`#${result.installation.id}`)}, kind=${row?.kind ?? "unknown"}).`
5469
+ );
5470
+ warn(
5471
+ `No public URL yet \u2014 runtime provisioning didn't complete. Re-running ${code("dashwise module install")} is idempotent and repairs a failed provision.`
5472
+ );
5473
+ }
5474
+ return 0;
5475
+ }
5476
+ function resolveSlug(slugArg, dir) {
5477
+ if (slugArg) return slugArg;
5478
+ try {
5479
+ const manifest = readManifest(dir);
5480
+ const slug = manifest?.module?.slug;
5481
+ return typeof slug === "string" && slug.length > 0 ? slug : void 0;
5482
+ } catch {
5483
+ return void 0;
5484
+ }
5485
+ }
5486
+ async function resolveDevInstallCollision(existing, slug, workspaceSlug, options) {
5487
+ if (existing.kind === "sandbox") {
5488
+ fail(
5489
+ `Workspace ${code(workspaceSlug)} already has a ${code("kind=sandbox")} install of ${code(slug)} (id=${existing.id}), and only ONE install per (workspace, module) can exist.
5490
+ A plain install would upgrade that row in place \u2014 but ${code("kind")} is immutable, so it would stay sandbox and hidden from the app sidebar.
5491
+ The CLI can't destroy a sandbox install. Uninstall it from the dashboard first, or deploy with ${code("--workspace <other>")} to a separate workspace.`
5492
+ );
5493
+ return 1;
5494
+ }
5495
+ const explanation = `Workspace ${code(workspaceSlug)} already has the DEV install of ${code(slug)} (kind=local, id=${existing.id}) created by ${code("dashwise init")}.
5496
+ Only ONE install per (workspace, module) can exist, and ${code("kind")} is immutable: a plain install would upgrade that row in place \u2014 the runtime would provision, but it would STAY a dev install, hidden from the app sidebar.
5497
+ Options:
5498
+ - ${code("--replace-dev-install")} \u2014 destroy the dev install, then fresh production install. Recommended when development is done. Note: this revokes the module's cached local-dev credentials, so local ${code("dashwise dev")} against this workspace needs a fresh ${code("dashwise init --force")} afterwards (which in turn cannot coexist with the production install here).
5499
+ - ${code("--workspace <other>")} \u2014 deploy to a separate test/client workspace and keep this dev install. Recommended for ongoing development.`;
5500
+ if (options.replaceDevInstall) return "replace";
5501
+ if (process.stdout.isTTY && !options.yes) {
5502
+ info(explanation);
5503
+ const answer = await select({
5504
+ message: `Replace the dev install of ${code(slug)} in ${code(workspaceSlug)} with a production install? (revokes local-dev credentials)`,
5505
+ options: [
5506
+ {
5507
+ value: "replace",
5508
+ label: "Replace \u2014 destroy the dev install, then production-install",
5509
+ hint: "local `dashwise dev` here will need `dashwise init --force` afterwards"
5510
+ },
5511
+ { value: "cancel", label: "Cancel \u2014 I will deploy to another workspace" }
5512
+ ],
5513
+ initialValue: "cancel"
5514
+ });
5515
+ if (isCancel(answer) || answer === "cancel") {
5516
+ fail("Aborted. Nothing was changed.");
5517
+ return 130;
5518
+ }
5519
+ return "replace-confirmed";
5520
+ }
5521
+ fail(explanation);
5522
+ return 1;
5523
+ }
5524
+ async function replaceDevInstall(existing, slug, auth) {
5525
+ info(`Destroying dev install ${code(`#${existing.id}`)} (kind=local)\u2026`);
5526
+ try {
5527
+ await destroyDevInstallation(existing.id, auth);
5528
+ } catch (err) {
5529
+ if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
5530
+ warn("Dev install already gone server-side \u2014 continuing.");
5531
+ } else if (err instanceof ApiError) {
5532
+ fail(
5533
+ `Could not destroy the dev install (${err.code} ${err.status}): ${err.message}`
5534
+ );
5535
+ return 1;
5536
+ } else {
5537
+ fail(`Could not destroy the dev install: ${err.message}`);
5538
+ return 1;
5539
+ }
5540
+ }
5541
+ const entry = readModuleEntry(slug);
5542
+ if (entry && entry.installationId === existing.id) {
5543
+ try {
5544
+ removeModuleEntry(slug);
5545
+ info(
5546
+ `Removed ${code(slug)} from auth.json \u2014 its local-dev credentials are revoked. Local ${code("dashwise dev")} needs ${code("dashwise init --force")} (ideally against a different workspace).`
5547
+ );
5548
+ } catch (err) {
5549
+ warn(
5550
+ `Dev install destroyed but auth.json cleanup failed: ${err.message}. Remove the ${code(`modules["${slug}"]`)} entry from ${code("~/.config/dashwise/auth.json")} manually.`
5551
+ );
5552
+ }
5553
+ }
5554
+ return 0;
5555
+ }
5556
+ function surfaceInstallError(err, slug, workspaceSlug, devInstallReplaced) {
5557
+ if (err instanceof ApiError) {
5558
+ switch (err.code) {
5559
+ case "INSTALL_NOT_INVITED":
5560
+ fail(
5561
+ `Module ${code(slug)} is invite-only and workspace ${code(workspaceSlug)} hasn't been invited.
5562
+ Ask the module's author to invite this workspace (or open the install gate), then retry.`
5563
+ );
5564
+ break;
5565
+ case "UPGRADE_BREAKS_CONSUMERS":
5566
+ fail(
5567
+ `Upgrading ${code(slug)} would break modules that consume its current schema.
5568
+ Review the breaking changes, then re-run with ${code("--force")} to upgrade anyway.`
5569
+ );
5570
+ break;
5571
+ case "FORBIDDEN":
5572
+ fail(
5573
+ `You must be an ADMIN of workspace ${code(workspaceSlug)} to install modules into it.`
5574
+ );
5575
+ break;
5576
+ case "UNAUTHENTICATED":
5577
+ fail(
5578
+ `Auth rejected by the backend \u2014 your CLI token may be stale. Re-run ${code("dashwise login")} and try again.`
5579
+ );
5580
+ break;
5581
+ default:
5582
+ if (err.status === 400 && /publish/i.test(err.message)) {
5583
+ fail(
5584
+ `${err.message}
5585
+ Run ${code("dashwise module publish")} first, then retry.`
5586
+ );
5587
+ } else {
5588
+ fail(`Install failed (${err.code || err.status}): ${err.message}`);
5589
+ }
5590
+ }
5591
+ } else {
5592
+ fail(`Install failed: ${err.message}`);
5593
+ }
5594
+ if (devInstallReplaced) {
5595
+ warn(
5596
+ `Note: the dev install was already removed (that is not rolled back). Re-run ${code("dashwise module install")} to complete the production install, or ${code("dashwise init --force")} to restore local dev.`
5597
+ );
5598
+ }
5599
+ return 1;
5600
+ }
5601
+
5203
5602
  // src/commands/module/list.ts
5204
5603
  init_api_client();
5205
5604
  init_config();
@@ -5668,6 +6067,140 @@ function formatBytes2(n) {
5668
6067
  return `${(n / 1024 / 1024).toFixed(1)}MB`;
5669
6068
  }
5670
6069
 
6070
+ // src/commands/module/status.ts
6071
+ init_api_client();
6072
+ init_config();
6073
+ init_manifest_io();
6074
+ init_output();
6075
+ async function moduleStatusCommand(slugArg, options) {
6076
+ let slug = slugArg;
6077
+ if (!slug) {
6078
+ try {
6079
+ const manifest = readManifest(options.dir);
6080
+ const fromManifest = manifest?.module?.slug;
6081
+ if (typeof fromManifest === "string" && fromManifest.length > 0) {
6082
+ slug = fromManifest;
6083
+ }
6084
+ } catch {
6085
+ }
6086
+ }
6087
+ if (!slug) {
6088
+ fail(
6089
+ `No module slug \u2014 pass one (${code("dashwise module status <slug>")}) or run from a directory with a valid module.json.`
6090
+ );
6091
+ return 1;
6092
+ }
6093
+ if (!isLoggedIn()) {
6094
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
6095
+ return 1;
6096
+ }
6097
+ const cfg = readConfig();
6098
+ if (!cfg) {
6099
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
6100
+ return 1;
6101
+ }
6102
+ const auth = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
6103
+ let mod;
6104
+ try {
6105
+ mod = await getModuleBySlug(slug, auth);
6106
+ } catch (err) {
6107
+ if (err instanceof ApiError && err.status === 404) {
6108
+ fail(`Module ${code(slug)} not found on the backend.`);
6109
+ return 1;
6110
+ }
6111
+ fail(`Could not resolve module ${code(slug)}: ${err.message}`);
6112
+ return 1;
6113
+ }
6114
+ let workspace2;
6115
+ try {
6116
+ workspace2 = await resolveWorkspaceWithFallback({
6117
+ userFlag: options.workspace,
6118
+ cachedWorkspaceId: readModuleEntry(slug)?.workspaceId,
6119
+ configDefault: cfg.workspaceSlug,
6120
+ apiUrl: resolveApiUrl(),
6121
+ apiToken: cfg.token
6122
+ });
6123
+ } catch (err) {
6124
+ fail(`Workspace selection failed: ${err.message}`);
6125
+ return 1;
6126
+ }
6127
+ let row;
6128
+ try {
6129
+ const installs = await listWorkspaceInstallations(workspace2.id, auth);
6130
+ row = installs.find((r) => r.module_id === mod.id);
6131
+ } catch (err) {
6132
+ fail(`Could not read workspace installations: ${err.message}`);
6133
+ return 1;
6134
+ }
6135
+ const latestVersion = row?.module?.current_version ?? mod.current_version;
6136
+ if (!row) {
6137
+ if (options.json) {
6138
+ out(
6139
+ JSON.stringify({
6140
+ installed: false,
6141
+ module: mod.slug,
6142
+ workspace: { id: workspace2.id, slug: workspace2.slug },
6143
+ latest_version: latestVersion
6144
+ })
6145
+ );
6146
+ return 0;
6147
+ }
6148
+ info(
6149
+ `${code(mod.slug)} is not installed in workspace ${code(workspace2.slug)}.` + (latestVersion ? ` Deploy it with ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)}.` : ` It has no published version yet \u2014 run ${code("dashwise module publish")} first.`)
6150
+ );
6151
+ return 0;
6152
+ }
6153
+ const runtimeProvisioned = isRealRuntimeUrl(row.runtime_url);
6154
+ const state = runtimeProvisioned && row.public_url ? "live" : runtimeProvisioned ? "deployed" : "not_deployed";
6155
+ const installedVersion = row.module_version?.version ?? null;
6156
+ const updateAvailable = isUpdateAvailable(installedVersion, latestVersion);
6157
+ if (options.json) {
6158
+ out(
6159
+ JSON.stringify({
6160
+ installed: true,
6161
+ module: mod.slug,
6162
+ workspace: { id: workspace2.id, slug: workspace2.slug },
6163
+ installation_id: row.id,
6164
+ kind: row.kind ?? null,
6165
+ version: installedVersion,
6166
+ latest_version: latestVersion,
6167
+ update_available: updateAvailable,
6168
+ runtime_provisioned: runtimeProvisioned,
6169
+ runtime_url: row.runtime_url,
6170
+ public_url: row.public_url,
6171
+ state
6172
+ })
6173
+ );
6174
+ return 0;
6175
+ }
6176
+ const stateLabel = state === "live" ? "Live" : state === "deployed" ? "Deployed" : "Not deployed \u2014 runtime not provisioned";
6177
+ out("");
6178
+ kv("Module:", `${mod.slug} ${dim(`(id ${mod.id})`)}`);
6179
+ kv("Workspace:", `${workspace2.slug} ${dim(`(id ${workspace2.id})`)}`);
6180
+ kv("Install:", `#${row.id} ${dim(`(kind=${row.kind ?? "unknown"})`)}`);
6181
+ kv(
6182
+ "Version:",
6183
+ `${installedVersion ?? dim("(unknown)")}${updateAvailable ? ` ${code(`update available: v${latestVersion} \u2014 run \`dashwise module install ${mod.slug}\``)}` : latestVersion ? ` ${dim(`(latest published: v${latestVersion})`)}` : ""}`
6184
+ );
6185
+ kv(
6186
+ "Runtime:",
6187
+ runtimeProvisioned ? `provisioned ${dim(`(${row.runtime_url})`)}` : "not provisioned"
6188
+ );
6189
+ kv("Public URL:", row.public_url ?? dim("(none)"));
6190
+ kv("State:", stateLabel);
6191
+ out("");
6192
+ if (row.kind === "local") {
6193
+ info(
6194
+ `This is the ${code("kind=local")} dev install \u2014 hidden from the app sidebar. A production deploy needs ${code("dashwise module install --replace-dev-install")} (or ${code("--workspace <other>")}).`
6195
+ );
6196
+ } else if (state === "not_deployed") {
6197
+ info(
6198
+ `Re-run ${code(`dashwise module install ${mod.slug} --workspace ${workspace2.slug}`)} to (re-)provision the runtime \u2014 the install POST is idempotent.`
6199
+ );
6200
+ }
6201
+ return 0;
6202
+ }
6203
+
5671
6204
  // src/commands/module/pull-data.ts
5672
6205
  init_api_client();
5673
6206
  init_output();
@@ -6289,7 +6822,7 @@ function rangeSatisfied(version, range) {
6289
6822
  const op = opMatch[1] ?? "=";
6290
6823
  const target = parseSemver(opMatch[2]);
6291
6824
  if (!target) return false;
6292
- const cmp = compareSemver(vParts, target);
6825
+ const cmp = compareSemver2(vParts, target);
6293
6826
  switch (op) {
6294
6827
  case "=":
6295
6828
  return cmp === 0;
@@ -6318,7 +6851,7 @@ function parseSemver(s) {
6318
6851
  if (!m) return null;
6319
6852
  return [Number(m[1]), Number(m[2]), Number(m[3])];
6320
6853
  }
6321
- function compareSemver(a, b) {
6854
+ function compareSemver2(a, b) {
6322
6855
  for (let i = 0; i < 3; i++) {
6323
6856
  if (a[i] !== b[i]) return a[i] - b[i];
6324
6857
  }
@@ -9204,6 +9737,35 @@ moduleCmd.command("publish").description("Pack the current directory + upload it
9204
9737
  });
9205
9738
  }
9206
9739
  );
9740
+ moduleCmd.command("install [slug]").alias("deploy").description(
9741
+ "Install (deploy) a published module into a workspace \u2014 provisions the runtime and prints the public URL. Upgrades/repairs an existing production install idempotently. Refuses to silently land on a dev install: use --replace-dev-install or --workspace <other>."
9742
+ ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--version <semver>", "Install a specific published version (default: latest).").option("--force", "Proceed even when the upgrade breaks consumer modules (UPGRADE_BREAKS_CONSUMERS).").option(
9743
+ "--replace-dev-install",
9744
+ "Destroy the workspace's kind=local dev install first, then fresh production install. Revokes the module's cached local-dev credentials (re-run `dashwise init --force` for local dev afterwards)."
9745
+ ).option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("-y, --yes", "Skip the confirmation prompt (for CI / scripted use).").option("--json", "Emit the final result as machine-readable JSON.").action(
9746
+ async (slug, cmdOpts) => {
9747
+ process.exitCode = await moduleInstallCommand(slug, {
9748
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
9749
+ ...cmdOpts.version !== void 0 ? { version: cmdOpts.version } : {},
9750
+ ...cmdOpts.force !== void 0 ? { force: cmdOpts.force } : {},
9751
+ ...cmdOpts.replaceDevInstall !== void 0 ? { replaceDevInstall: cmdOpts.replaceDevInstall } : {},
9752
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
9753
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {},
9754
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
9755
+ });
9756
+ }
9757
+ );
9758
+ moduleCmd.command("status [slug]").description(
9759
+ "Show a module's deploy state in a workspace: install kind, installed vs latest published version, runtime provisioning, and the public URL."
9760
+ ).option("--workspace <slug-or-id>", "Target workspace (default: cached module workspace, config default, or interactive picker).").option("--dir <path>", "Project root for the module.json slug lookup (default: current directory).").option("--json", "Emit machine-readable JSON.").action(
9761
+ async (slug, cmdOpts) => {
9762
+ process.exitCode = await moduleStatusCommand(slug, {
9763
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
9764
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
9765
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
9766
+ });
9767
+ }
9768
+ );
9207
9769
  moduleCmd.command("query-log").description(
9208
9770
  "Read the per-module SDK query log. Default: 50 most recent entries. `--slow` filters to duration_ms > 500ms; `--since 1h` narrows by time; `--tail` streams new entries every 5s."
9209
9771
  ).option(
@@ -9381,7 +9943,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
9381
9943
  }
9382
9944
  })();
9383
9945
  function getVersion() {
9384
- return "0.12.1";
9946
+ return "0.13.0";
9385
9947
  }
9386
9948
  function parseIntOption(value) {
9387
9949
  const n = Number.parseInt(value, 10);