@dashai/cli 0.5.1 → 0.7.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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, watch, readdirSync, chmodSync, renameSync, mkdtempSync, statSync } from 'fs';
3
3
  import { tmpdir, platform, homedir } from 'os';
4
- import { resolve, join, dirname, relative } from 'path';
4
+ import { resolve, join, dirname, basename, relative } from 'path';
5
5
  import pc from 'picocolors';
6
6
  import { intro, log, select, isCancel, cancel, spinner, outro, text, multiselect, confirm } from '@clack/prompts';
7
7
  import { Command, Option } from 'commander';
@@ -1873,7 +1873,7 @@ async function selectWorkspace(opts) {
1873
1873
  if (workspaces.length === 1) {
1874
1874
  return workspaces[0];
1875
1875
  }
1876
- if (process.stdout.isTTY) {
1876
+ if (opts.interactive && process.stdout.isTTY) {
1877
1877
  const choice = await select({
1878
1878
  message: "Which workspace should this module live in?",
1879
1879
  options: workspaces.map((w) => ({
@@ -3223,13 +3223,13 @@ async function resolveDevPort(requested) {
3223
3223
  }
3224
3224
  async function isPortFree(port) {
3225
3225
  const { createServer } = await import('net');
3226
- return new Promise((resolve11) => {
3226
+ return new Promise((resolve12) => {
3227
3227
  const server = createServer();
3228
3228
  server.once("error", () => {
3229
- resolve11(false);
3229
+ resolve12(false);
3230
3230
  });
3231
3231
  server.once("listening", () => {
3232
- server.close(() => resolve11(true));
3232
+ server.close(() => resolve12(true));
3233
3233
  });
3234
3234
  server.listen(port, "0.0.0.0");
3235
3235
  });
@@ -3426,7 +3426,7 @@ async function loginCommand(options) {
3426
3426
  return 0;
3427
3427
  }
3428
3428
  function sleep(ms) {
3429
- return new Promise((resolve11) => setTimeout(resolve11, ms));
3429
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
3430
3430
  }
3431
3431
  function formatSeconds(seconds) {
3432
3432
  if (seconds < 60) return `${seconds}s`;
@@ -3884,6 +3884,336 @@ function isPlausibleRepoUrl(url) {
3884
3884
  return /^git@[^:]+:[^/]+\/.+/.test(url);
3885
3885
  }
3886
3886
 
3887
+ // src/commands/module/convert.ts
3888
+ init_scaffold();
3889
+ var SDK_DEP_RANGE = "^0.8.0";
3890
+ var SCAN_DIRS = ["app", "src", "pages", "lib", "components"];
3891
+ var SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
3892
+ var MAX_SCAN_FILES = 2e3;
3893
+ function deriveSlug(input) {
3894
+ return input.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
3895
+ }
3896
+ function convertModule(opts) {
3897
+ const dir = resolve(opts.dir);
3898
+ const pkgPath = join(dir, "package.json");
3899
+ if (!existsSync(pkgPath)) {
3900
+ throw new Error(
3901
+ "No package.json found \u2014 run `dashwise module convert` inside a Next.js project."
3902
+ );
3903
+ }
3904
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3905
+ const nextConfigFile = ["next.config.mjs", "next.config.js", "next.config.ts"].find(
3906
+ (f) => existsSync(join(dir, f))
3907
+ );
3908
+ const looksNext = Boolean(pkg.dependencies?.next || pkg.devDependencies?.next) || nextConfigFile !== void 0 || existsSync(join(dir, "app")) || existsSync(join(dir, "src", "app"));
3909
+ if (!looksNext) {
3910
+ throw new Error(
3911
+ "This does not look like a Next.js app (no `next` dependency, next.config, or app/ directory)."
3912
+ );
3913
+ }
3914
+ const slug = opts.slug ?? deriveSlug(pkg.name ?? basename(dir));
3915
+ if (!isValidSlug(slug)) {
3916
+ throw new Error(
3917
+ `Could not derive a valid module slug ("${slug}"). Pass --slug (must match /^[a-z][a-z0-9_-]*$/).`
3918
+ );
3919
+ }
3920
+ const name = opts.name ?? pkg.name ?? slug;
3921
+ const sopts = {
3922
+ slug,
3923
+ name,
3924
+ kind: "custom",
3925
+ ...opts.repositoryUrl ? { repositoryUrl: opts.repositoryUrl } : {}
3926
+ };
3927
+ const created = [];
3928
+ const skipped = [];
3929
+ const warnings = [];
3930
+ const writeIfMissing = (rel, content) => {
3931
+ const p = join(dir, rel);
3932
+ if (existsSync(p)) {
3933
+ skipped.push(rel);
3934
+ return;
3935
+ }
3936
+ mkdirSync(dirname(p), { recursive: true });
3937
+ writeFileSync(p, content, "utf-8");
3938
+ created.push(rel);
3939
+ };
3940
+ writeIfMissing("module.json", manifestJson(sopts));
3941
+ writeIfMissing("middleware.ts", middlewareTs());
3942
+ writeIfMissing("app/api/auth/sign-in/route.ts", appApiAuthSignInTs());
3943
+ writeIfMissing("app/api/auth/callback/route.ts", appApiAuthCallbackTs());
3944
+ writeIfMissing("app/api/auth/sign-out/route.ts", appApiAuthSignOutTs());
3945
+ writeIfMissing("app/sign-in-error/page.tsx", appSignInErrorPageTsx());
3946
+ writeIfMissing(".env.local.example", envLocalExample());
3947
+ const layoutFile = [
3948
+ "app/layout.tsx",
3949
+ "app/layout.jsx",
3950
+ "src/app/layout.tsx"
3951
+ ].find((f) => existsSync(join(dir, f)));
3952
+ if (!layoutFile) {
3953
+ writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
3954
+ } else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
3955
+ warnings.push(
3956
+ `${layoutFile}: wrap your tree in <AuthProvider> from "@dashai/sdk/auth/client" so client components can read the session.`
3957
+ );
3958
+ } else {
3959
+ skipped.push(layoutFile);
3960
+ }
3961
+ if (!nextConfigFile) {
3962
+ writeIfMissing("next.config.mjs", standaloneNextConfig());
3963
+ } else if (!readFileSync(join(dir, nextConfigFile), "utf-8").includes("standalone")) {
3964
+ warnings.push(
3965
+ `${nextConfigFile}: add \`output: 'standalone'\` \u2014 required for the module runtime bundle.`
3966
+ );
3967
+ }
3968
+ if (!pkg.dependencies?.["@dashai/sdk"]) {
3969
+ pkg.dependencies = { ...pkg.dependencies ?? {}, "@dashai/sdk": SDK_DEP_RANGE };
3970
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
3971
+ created.push("package.json (added @dashai/sdk)");
3972
+ }
3973
+ const outboundApis = scanOutboundHosts(dir);
3974
+ const runtimeBlockAdded = normalizeManifest(join(dir, "module.json"), {
3975
+ outboundApis
3976
+ });
3977
+ return {
3978
+ slug,
3979
+ name,
3980
+ created,
3981
+ skipped,
3982
+ warnings,
3983
+ outboundApis,
3984
+ runtimeBlockAdded
3985
+ };
3986
+ }
3987
+ function standaloneNextConfig() {
3988
+ return `/** @type {import("next").NextConfig} */
3989
+ const nextConfig = {
3990
+ output: 'standalone',
3991
+ typescript: { ignoreBuildErrors: true },
3992
+ eslint: { ignoreDuringBuilds: true },
3993
+ };
3994
+ export default nextConfig;
3995
+ `;
3996
+ }
3997
+ function normalizeManifest(manifestPath2, opts) {
3998
+ const manifest = JSON.parse(readFileSync(manifestPath2, "utf-8"));
3999
+ let runtimeAdded = false;
4000
+ if (manifest.module_kind === void 0) manifest.module_kind = "custom";
4001
+ let runtime = manifest.runtime;
4002
+ if (manifest.module_kind === "custom" && !runtime) {
4003
+ runtime = {
4004
+ type: "nextjs",
4005
+ kind: "machine",
4006
+ machine_size: "shared-cpu-1x",
4007
+ server_actions: true
4008
+ };
4009
+ manifest.runtime = runtime;
4010
+ runtimeAdded = true;
4011
+ }
4012
+ if (runtime && opts.outboundApis.length > 0) {
4013
+ const existing = Array.isArray(runtime.outbound_apis) ? runtime.outbound_apis : [];
4014
+ runtime.outbound_apis = Array.from(
4015
+ /* @__PURE__ */ new Set([...existing, ...opts.outboundApis])
4016
+ ).sort();
4017
+ }
4018
+ writeFileSync(manifestPath2, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
4019
+ return runtimeAdded;
4020
+ }
4021
+ function scanOutboundHosts(dir) {
4022
+ const hosts = /* @__PURE__ */ new Set();
4023
+ let scanned = 0;
4024
+ const hostRe = /https:\/\/([a-zA-Z0-9.-]+)/g;
4025
+ const walk2 = (abs) => {
4026
+ if (scanned >= MAX_SCAN_FILES) return;
4027
+ let entries;
4028
+ try {
4029
+ entries = readdirSync(abs);
4030
+ } catch {
4031
+ return;
4032
+ }
4033
+ for (const entry of entries) {
4034
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
4035
+ const p = join(abs, entry);
4036
+ let st;
4037
+ try {
4038
+ st = statSync(p);
4039
+ } catch {
4040
+ continue;
4041
+ }
4042
+ if (st.isDirectory()) {
4043
+ walk2(p);
4044
+ } else if (SCAN_EXTENSIONS.some((e) => entry.endsWith(e))) {
4045
+ if (scanned >= MAX_SCAN_FILES) return;
4046
+ scanned++;
4047
+ let body;
4048
+ try {
4049
+ body = readFileSync(p, "utf-8");
4050
+ } catch {
4051
+ continue;
4052
+ }
4053
+ for (const m of body.matchAll(hostRe)) {
4054
+ const host = m[1].toLowerCase();
4055
+ if (host === "localhost" || host.endsWith(".local")) continue;
4056
+ if (host.includes("dashwise")) continue;
4057
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(host)) continue;
4058
+ hosts.add(host);
4059
+ }
4060
+ }
4061
+ }
4062
+ };
4063
+ for (const d of SCAN_DIRS) {
4064
+ const abs = join(dir, d);
4065
+ if (existsSync(abs)) walk2(abs);
4066
+ }
4067
+ return Array.from(hosts).sort();
4068
+ }
4069
+ async function moduleConvertCommand(opts) {
4070
+ try {
4071
+ const result = convertModule({
4072
+ dir: opts.dir ?? process.cwd(),
4073
+ ...opts.slug !== void 0 ? { slug: opts.slug } : {},
4074
+ ...opts.name !== void 0 ? { name: opts.name } : {},
4075
+ ...opts.git !== void 0 ? { repositoryUrl: opts.git } : {}
4076
+ });
4077
+ console.log(`Converted "${result.name}" (slug: ${result.slug}) to a DashWise module.
4078
+ `);
4079
+ if (result.created.length) {
4080
+ console.log(" Created:");
4081
+ for (const f of result.created) console.log(` + ${f}`);
4082
+ }
4083
+ if (result.skipped.length) {
4084
+ console.log(" Left as-is:");
4085
+ for (const f of result.skipped) console.log(` \xB7 ${f}`);
4086
+ }
4087
+ if (result.runtimeBlockAdded) {
4088
+ console.log(" + module.json: added runtime block (nextjs/machine)");
4089
+ }
4090
+ if (result.outboundApis.length) {
4091
+ console.log(
4092
+ ` + module.json runtime.outbound_apis: ${result.outboundApis.join(", ")}`
4093
+ );
4094
+ }
4095
+ if (result.warnings.length) {
4096
+ console.log("\n Manual steps needed:");
4097
+ for (const w of result.warnings) console.log(` ! ${w}`);
4098
+ }
4099
+ console.log(
4100
+ "\nNext: `npm install`, then `dashwise dev` to run it against DashWise, or `dashwise module publish` to deploy."
4101
+ );
4102
+ return 0;
4103
+ } catch (err) {
4104
+ console.error(
4105
+ `convert failed: ${err instanceof Error ? err.message : String(err)}`
4106
+ );
4107
+ return 1;
4108
+ }
4109
+ }
4110
+
4111
+ // src/commands/module/register.ts
4112
+ init_config();
4113
+ init_output();
4114
+ init_manifest_io();
4115
+ init_workspaces();
4116
+ init_api_client();
4117
+ var MODULE_KINDS = ["ai_builder", "hand_authored", "custom"];
4118
+ async function moduleRegisterCommand(opts) {
4119
+ let manifest;
4120
+ try {
4121
+ manifest = readManifest(opts.dir);
4122
+ } catch (err) {
4123
+ fail(`Could not read module.json: ${err.message}`);
4124
+ return 1;
4125
+ }
4126
+ if (!manifest) {
4127
+ fail(
4128
+ `No module.json found${opts.dir ? ` in ${opts.dir}` : " in the current directory"}. Run ${code("dashwise module convert")} (existing app) or ${code("dashwise init")} (new) first.`
4129
+ );
4130
+ return 1;
4131
+ }
4132
+ const slug = manifest.module?.slug;
4133
+ if (!slug || typeof slug !== "string") {
4134
+ fail("module.json is missing `module.slug`.");
4135
+ return 1;
4136
+ }
4137
+ const name = opts.name ?? (typeof manifest.module?.name === "string" ? manifest.module.name : slug);
4138
+ const moduleKind = MODULE_KINDS.includes(
4139
+ manifest.module_kind
4140
+ ) ? manifest.module_kind : void 0;
4141
+ const description = typeof manifest.module?.description === "string" ? manifest.module.description : void 0;
4142
+ if (!isLoggedIn()) {
4143
+ fail(`Not logged in. Run ${code("dashwise login")} first.`);
4144
+ return 1;
4145
+ }
4146
+ const cfg = readConfig();
4147
+ if (!cfg) {
4148
+ fail(`CLI config missing. Run ${code("dashwise login")}.`);
4149
+ return 1;
4150
+ }
4151
+ let workspace2;
4152
+ try {
4153
+ workspace2 = await selectWorkspace({
4154
+ apiUrl: resolveApiUrl(),
4155
+ apiToken: cfg.token,
4156
+ ...opts.workspace !== void 0 ? { flag: opts.workspace } : {},
4157
+ interactive: true
4158
+ });
4159
+ } catch (err) {
4160
+ fail(`Workspace selection failed: ${err.message}`);
4161
+ return 1;
4162
+ }
4163
+ info(
4164
+ `Registering "${name}" (slug: ${code(slug)}) in workspace ${code(workspace2.slug)} (id=${workspace2.id})\u2026`
4165
+ );
4166
+ let response;
4167
+ try {
4168
+ response = await createDevInstallation(
4169
+ {
4170
+ slug,
4171
+ name,
4172
+ ...description !== void 0 ? { description } : {},
4173
+ workspaceId: workspace2.id,
4174
+ ...moduleKind !== void 0 ? { moduleKind } : {}
4175
+ },
4176
+ { baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
4177
+ );
4178
+ } catch (err) {
4179
+ if (err instanceof ApiError) {
4180
+ if (err.code === "SLUG_ALREADY_EXISTS") {
4181
+ fail(
4182
+ `A module with slug "${slug}" already exists. Pick a different slug in module.json, or run ${code("dashwise module destroy-dev-install")} to reclaim it.`
4183
+ );
4184
+ } else if (err.code === "FORBIDDEN") {
4185
+ fail(`Not a member of workspace "${workspace2.slug}". Join it, then retry.`);
4186
+ } else {
4187
+ fail(`Registration failed (${err.code || err.status}): ${err.message}`);
4188
+ }
4189
+ return 1;
4190
+ }
4191
+ fail(`Registration failed: ${err.message}`);
4192
+ return 1;
4193
+ }
4194
+ try {
4195
+ upsertModuleEntry(slug, {
4196
+ runtimeToken: response.runtime_token,
4197
+ installationId: response.installation.id,
4198
+ expiresAt: response.runtime_token_expires_at
4199
+ });
4200
+ } catch (err) {
4201
+ warn(
4202
+ `Registered (installation id=${response.installation.id}) but failed to cache the runtime token: ${err.message}
4203
+ Runtime token (save this manually): ${response.runtime_token}
4204
+ Expires: ${response.runtime_token_expires_at}`
4205
+ );
4206
+ return 0;
4207
+ }
4208
+ success(
4209
+ `Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}. Runtime token cached.`
4210
+ );
4211
+ info(
4212
+ `Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy. Add tables with ${code("dashwise table create")}.`
4213
+ );
4214
+ return 0;
4215
+ }
4216
+
3887
4217
  // src/commands/module/generate.ts
3888
4218
  init_api_client();
3889
4219
  init_output();
@@ -6126,7 +6456,7 @@ function formatTimestamp(iso) {
6126
6456
  return iso.slice(0, 19).replace("T", " ");
6127
6457
  }
6128
6458
  function sleep2(ms) {
6129
- return new Promise((resolve11) => setTimeout(resolve11, ms));
6459
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
6130
6460
  }
6131
6461
 
6132
6462
  // src/commands/module/action-log.ts
@@ -6220,7 +6550,7 @@ async function runTail2(installationId, options) {
6220
6550
  }
6221
6551
  dim(`(tail: transient error \u2014 ${err.message})`);
6222
6552
  }
6223
- await new Promise((resolve11) => setTimeout(resolve11, POLL_INTERVAL_MS2));
6553
+ await new Promise((resolve12) => setTimeout(resolve12, POLL_INTERVAL_MS2));
6224
6554
  }
6225
6555
  }
6226
6556
  function printTable(rows) {
@@ -6414,6 +6744,29 @@ moduleCmd.command("connect-git <repo-url>").description("Link the local module t
6414
6744
  });
6415
6745
  }
6416
6746
  );
6747
+ moduleCmd.command("convert [dir]").description(
6748
+ "Convert an EXISTING Next.js app into a DashWise module \u2014 idempotently inject module.json, auth wiring, the SDK dep, and output:standalone. Creates missing files; warns (never overwrites) for an existing layout/next.config/module.json."
6749
+ ).option("--slug <slug>", "Module slug (default: derived from package.json name / dir).").option("--name <name>", "Human-readable module name (default: package.json name).").option("--git <url>", "Git repository URL \u2014 written to module.json#repository.url.").action(
6750
+ async (dir, cmdOpts) => {
6751
+ process.exitCode = await moduleConvertCommand({
6752
+ ...dir !== void 0 ? { dir } : {},
6753
+ ...cmdOpts.slug !== void 0 ? { slug: cmdOpts.slug } : {},
6754
+ ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
6755
+ ...cmdOpts.git !== void 0 ? { git: cmdOpts.git } : {}
6756
+ });
6757
+ }
6758
+ );
6759
+ moduleCmd.command("register [dir]").description(
6760
+ "Link an existing local module (a directory with module.json \u2014 e.g. after `module convert`) to DashWise: creates the backend module record + a dev install and caches the runtime token, so `dashwise dev` / `publish` work. Requires login."
6761
+ ).option("--workspace <slug-or-id>", "Target workspace (default: interactive picker).").option("--name <name>", "Override the module name (default: module.json name).").action(
6762
+ async (dir, cmdOpts) => {
6763
+ process.exitCode = await moduleRegisterCommand({
6764
+ ...dir !== void 0 ? { dir } : {},
6765
+ ...cmdOpts.workspace !== void 0 ? { workspace: cmdOpts.workspace } : {},
6766
+ ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {}
6767
+ });
6768
+ }
6769
+ );
6417
6770
  moduleCmd.command("generate").description("Regenerate typed wrappers from the local module.json into node_modules/@dashai/generated/.").option("--dir <path>", "Project root (default: current directory).").option("--dry-run", "Print summary without writing files.").action(async (cmdOpts) => {
6418
6771
  process.exitCode = await moduleGenerateCommand({
6419
6772
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
@@ -6779,7 +7132,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
6779
7132
  }
6780
7133
  })();
6781
7134
  function getVersion() {
6782
- return "0.5.1";
7135
+ return "0.7.0";
6783
7136
  }
6784
7137
  function parseIntOption(value) {
6785
7138
  const n = Number.parseInt(value, 10);