@construct-space/cli 1.0.2 → 1.0.5

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.js CHANGED
@@ -4576,7 +4576,7 @@ function toDisplayName(name) {
4576
4576
  // src/commands/scaffold.ts
4577
4577
  var nameRegex = /^[a-z][a-z0-9-]*$/;
4578
4578
  function render(template, data) {
4579
- return template.replace(/\{\{\.Name\}\}/g, data.name).replace(/\{\{\.ID\}\}/g, data.id).replace(/\{\{\.DisplayName\}\}/g, data.displayName).replace(/\{\{\.DisplayNameNoSpace\}\}/g, data.displayNameNoSpace);
4579
+ return template.replace(/\{\{\.Name\}\}/g, data.name).replace(/\{\{\.ID\}\}/g, data.id).replace(/\{\{\.IDUpper\}\}/g, data.idUpper).replace(/\{\{\.DisplayName\}\}/g, data.displayName).replace(/\{\{\.DisplayNameNoSpace\}\}/g, data.displayNameNoSpace);
4580
4580
  }
4581
4581
  function writeTemplate(templateDir, tmplName, outPath, data) {
4582
4582
  const tmplPath = join(templateDir, tmplName);
@@ -4609,6 +4609,7 @@ async function scaffold(nameArg, options) {
4609
4609
  const data = {
4610
4610
  name,
4611
4611
  id,
4612
+ idUpper: id.toUpperCase().replace(/[^A-Z0-9]/g, "_"),
4612
4613
  displayName,
4613
4614
  displayNameNoSpace: displayName.replace(/ /g, "")
4614
4615
  };
@@ -7591,8 +7592,9 @@ async function build(options) {
7591
7592
  }
7592
7593
 
7593
7594
  // src/commands/dev.ts
7594
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, unlinkSync, cpSync } from "fs";
7595
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5, unlinkSync, readFileSync as readFileSync5, cpSync } from "fs";
7595
7596
  import { join as join10 } from "path";
7597
+ import { createHash as createHash2 } from "crypto";
7596
7598
 
7597
7599
  // node_modules/chokidar/esm/index.js
7598
7600
  import { stat as statcb } from "fs";
@@ -9262,18 +9264,34 @@ async function dev() {
9262
9264
  }
9263
9265
  });
9264
9266
  const distDir = join10(root, "dist");
9265
- const distWatcher = watch(distDir, { ignoreInitial: true, depth: 1 });
9267
+ const bundleFile = join10(distDir, `space-${m.id}.iife.js`);
9268
+ let lastChecksum = "";
9269
+ const distWatcher = watch(bundleFile, { ignoreInitial: false });
9266
9270
  distWatcher.on("all", () => {
9267
- if (existsSync7(distDir)) {
9268
- mkdirSync3(installDir, { recursive: true });
9269
- cpSync(distDir, installDir, { recursive: true });
9270
- const devInstall = devSpaceDir(m.id);
9271
- const devParent = join10(devInstall, "..");
9272
- if (existsSync7(devParent)) {
9273
- mkdirSync3(devInstall, { recursive: true });
9274
- cpSync(distDir, devInstall, { recursive: true });
9275
- }
9276
- }
9271
+ if (!existsSync7(bundleFile))
9272
+ return;
9273
+ const bundleData = readFileSync5(bundleFile);
9274
+ const checksum = createHash2("sha256").update(bundleData).digest("hex");
9275
+ if (checksum === lastChecksum)
9276
+ return;
9277
+ lastChecksum = checksum;
9278
+ const raw = readRaw(root);
9279
+ writeWithBuild(distDir, raw, {
9280
+ checksum,
9281
+ size: bundleData.length,
9282
+ hostApiVersion: "0.2.0",
9283
+ builtAt: new Date().toISOString()
9284
+ });
9285
+ mkdirSync3(installDir, { recursive: true });
9286
+ cpSync(distDir, installDir, { recursive: true });
9287
+ writeFileSync5(join10(installDir, ".dev"), "dev");
9288
+ const devInstall = devSpaceDir(m.id);
9289
+ const devParent = join10(devInstall, "..");
9290
+ if (existsSync7(devParent)) {
9291
+ mkdirSync3(devInstall, { recursive: true });
9292
+ cpSync(distDir, devInstall, { recursive: true });
9293
+ }
9294
+ console.log(source_default.green(`Installed → ${m.id}`));
9277
9295
  });
9278
9296
  console.log(source_default.green("Watching for changes... (Ctrl+C to stop)"));
9279
9297
  await new Promise(() => {});
@@ -9306,7 +9324,7 @@ function run() {
9306
9324
  }
9307
9325
 
9308
9326
  // src/commands/publish.ts
9309
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, statSync as statSync5, unlinkSync as unlinkSync3 } from "fs";
9327
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, statSync as statSync6, unlinkSync as unlinkSync3 } from "fs";
9310
9328
  import { join as join14, basename as basename6 } from "path";
9311
9329
 
9312
9330
  // src/lib/auth.ts
@@ -9349,7 +9367,7 @@ function clear() {
9349
9367
  }
9350
9368
 
9351
9369
  // src/lib/pack.ts
9352
- import { readdirSync as readdirSync4, statSync as statSync4, existsSync as existsSync10 } from "fs";
9370
+ import { readdirSync as readdirSync3, statSync as statSync5, existsSync as existsSync10 } from "fs";
9353
9371
  import { join as join13 } from "path";
9354
9372
  import { tmpdir } from "os";
9355
9373
  import { execSync as execSync3 } from "child_process";
@@ -9391,8 +9409,8 @@ async function packSource(root) {
9391
9409
  if (existsSync10(join13(root, name)))
9392
9410
  entries.push(name);
9393
9411
  }
9394
- for (const entry of readdirSync4(root)) {
9395
- if (statSync4(join13(root, entry)).isDirectory())
9412
+ for (const entry of readdirSync3(root)) {
9413
+ if (statSync5(join13(root, entry)).isDirectory())
9396
9414
  continue;
9397
9415
  if (allowedRootFiles.includes(entry))
9398
9416
  continue;
@@ -9412,7 +9430,7 @@ async function packSource(root) {
9412
9430
  const excludes = "--exclude=node_modules --exclude=dist --exclude=.git --exclude=*.env --exclude=*.log --exclude=*.lock --exclude=*.lockb";
9413
9431
  const cmd = `tar czf "${tarballPath}" ${excludes} ${validEntries.join(" ")}`;
9414
9432
  execSync3(cmd, { cwd: root });
9415
- const size = statSync4(tarballPath).size;
9433
+ const size = statSync5(tarballPath).size;
9416
9434
  if (size > MAX_SIZE) {
9417
9435
  throw new Error(`Source exceeds maximum size of ${MAX_SIZE / 1024 / 1024}MB`);
9418
9436
  }
@@ -9552,7 +9570,7 @@ async function publish(options) {
9552
9570
  let tarballPath;
9553
9571
  try {
9554
9572
  tarballPath = await packSource(root);
9555
- const size = statSync5(tarballPath).size;
9573
+ const size = statSync6(tarballPath).size;
9556
9574
  spinner.succeed(`Source packed (${formatBytes(size)})`);
9557
9575
  } catch (err) {
9558
9576
  spinner.fail("Pack failed");
@@ -9815,11 +9833,12 @@ function logout() {
9815
9833
 
9816
9834
  // src/commands/update.ts
9817
9835
  import { execSync as execSync5 } from "child_process";
9836
+ var PKG_NAME = "@construct-space/cli";
9818
9837
  function update() {
9819
9838
  console.log(source_default.blue(`Current version: ${VERSION}`));
9820
9839
  let latest = VERSION;
9821
9840
  try {
9822
- latest = execSync5("npm view construct version", { encoding: "utf-8" }).trim();
9841
+ latest = execSync5(`npm view ${PKG_NAME} version`, { encoding: "utf-8" }).trim();
9823
9842
  } catch {}
9824
9843
  if (latest === VERSION) {
9825
9844
  console.log(source_default.green("Already on the latest version."));
@@ -9829,9 +9848,9 @@ function update() {
9829
9848
  const rt = detect();
9830
9849
  try {
9831
9850
  if (rt.name === "bun") {
9832
- execSync5("bun install -g construct", { stdio: "inherit" });
9851
+ execSync5(`bun install -g ${PKG_NAME}@latest`, { stdio: "inherit" });
9833
9852
  } else {
9834
- execSync5("npm install -g construct", { stdio: "inherit" });
9853
+ execSync5(`npm install -g ${PKG_NAME}@latest`, { stdio: "inherit" });
9835
9854
  }
9836
9855
  console.log(source_default.green(`Updated to v${latest}`));
9837
9856
  } catch (err) {
@@ -10084,7 +10103,7 @@ function updateBarrel(modelsDir, modelName) {
10084
10103
  }
10085
10104
 
10086
10105
  // src/commands/graph/push.ts
10087
- import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
10106
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
10088
10107
  import { join as join20, basename as basename7 } from "path";
10089
10108
  async function graphPush() {
10090
10109
  const root = process.cwd();
@@ -10098,7 +10117,7 @@ async function graphPush() {
10098
10117
  console.error(source_default.red("No src/models/ directory found. Run 'construct graph init' first."));
10099
10118
  process.exit(1);
10100
10119
  }
10101
- const modelFiles = readdirSync5(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10120
+ const modelFiles = readdirSync4(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10102
10121
  if (modelFiles.length === 0) {
10103
10122
  console.error(source_default.red("No model files found in src/models/"));
10104
10123
  console.log(source_default.dim(" Generate one: construct graph g User name:string email:string"));
@@ -10222,6 +10241,150 @@ function parseModelFile(content, fileName) {
10222
10241
  return result;
10223
10242
  }
10224
10243
 
10244
+ // src/commands/graph/migrate.ts
10245
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
10246
+ import { join as join21, basename as basename8 } from "path";
10247
+ async function graphMigrate(options) {
10248
+ const root = process.cwd();
10249
+ if (!exists(root)) {
10250
+ console.error(source_default.red("No space.manifest.json found in current directory"));
10251
+ process.exit(1);
10252
+ }
10253
+ const m = read(root);
10254
+ const modelsDir = join21(root, "src", "models");
10255
+ if (!existsSync17(modelsDir)) {
10256
+ console.error(source_default.red("No src/models/ directory. Run 'construct graph init' first."));
10257
+ process.exit(1);
10258
+ }
10259
+ let creds;
10260
+ try {
10261
+ creds = load2();
10262
+ } catch (err) {
10263
+ console.error(source_default.red(err.message));
10264
+ process.exit(1);
10265
+ }
10266
+ const graphURL = process.env.GRAPH_URL || "https://graph.construct.space";
10267
+ const spinner = ora("Fetching current schema...").start();
10268
+ let serverModels = [];
10269
+ try {
10270
+ const resp = await fetch(`${graphURL}/api/schemas/${m.id}`, {
10271
+ headers: { Authorization: `Bearer ${creds.token}` }
10272
+ });
10273
+ if (resp.ok) {
10274
+ const data = await resp.json();
10275
+ serverModels = data.models || [];
10276
+ }
10277
+ spinner.succeed("Schema fetched");
10278
+ } catch {
10279
+ spinner.fail("Could not fetch schema");
10280
+ process.exit(1);
10281
+ }
10282
+ const modelFiles = readdirSync5(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10283
+ const localModels = [];
10284
+ for (const file of modelFiles) {
10285
+ const content = readFileSync13(join21(modelsDir, file), "utf-8");
10286
+ const model = parseModelFields(content, basename8(file, ".ts"));
10287
+ if (model)
10288
+ localModels.push(model);
10289
+ }
10290
+ console.log();
10291
+ let hasChanges = false;
10292
+ for (const server of serverModels) {
10293
+ const local = localModels.find((m2) => m2.name === server.name);
10294
+ const serverFields = (server.fields || []).map((f) => f.name);
10295
+ if (!local) {
10296
+ console.log(source_default.yellow(` Model "${server.name}" exists on server but not locally`));
10297
+ hasChanges = true;
10298
+ continue;
10299
+ }
10300
+ const localFields = local.fields.map((f) => f.name);
10301
+ for (const sf of serverFields) {
10302
+ if (!localFields.includes(sf)) {
10303
+ console.log(source_default.red(` - ${server.name}.${sf}`), source_default.dim("(on server, not in local model — can drop)"));
10304
+ hasChanges = true;
10305
+ }
10306
+ }
10307
+ for (const lf of localFields) {
10308
+ if (!serverFields.includes(lf)) {
10309
+ console.log(source_default.green(` + ${server.name}.${lf}`), source_default.dim("(new — will be added on push)"));
10310
+ hasChanges = true;
10311
+ }
10312
+ }
10313
+ }
10314
+ for (const local of localModels) {
10315
+ if (!serverModels.find((m2) => m2.name === local.name)) {
10316
+ console.log(source_default.green(` + Model "${local.name}"`), source_default.dim("(new — will be created on push)"));
10317
+ hasChanges = true;
10318
+ }
10319
+ }
10320
+ if (!hasChanges) {
10321
+ console.log(source_default.green(" Schema is in sync — no changes needed"));
10322
+ return;
10323
+ }
10324
+ console.log();
10325
+ if (!options?.apply) {
10326
+ console.log(source_default.dim(" Run with --apply to apply destructive changes"));
10327
+ console.log(source_default.dim(' Or run "construct graph push" to add new fields/models'));
10328
+ return;
10329
+ }
10330
+ const proceed = await dist_default4({ message: "Apply destructive schema changes? This cannot be undone." });
10331
+ if (!proceed) {
10332
+ console.log("Cancelled.");
10333
+ return;
10334
+ }
10335
+ const migrateSpinner = ora("Applying migrations...").start();
10336
+ try {
10337
+ const resp = await fetch(`${graphURL}/api/schemas/migrate`, {
10338
+ method: "POST",
10339
+ headers: {
10340
+ "Content-Type": "application/json",
10341
+ Authorization: `Bearer ${creds.token}`,
10342
+ "X-Space-ID": m.id
10343
+ },
10344
+ body: JSON.stringify({
10345
+ space_id: m.id,
10346
+ project_id: "default",
10347
+ local_models: localModels
10348
+ })
10349
+ });
10350
+ if (!resp.ok) {
10351
+ const body = await resp.text();
10352
+ migrateSpinner.fail("Migration failed");
10353
+ console.error(source_default.red(` ${resp.status}: ${body}`));
10354
+ process.exit(1);
10355
+ }
10356
+ const result = await resp.json();
10357
+ migrateSpinner.succeed("Migrations applied");
10358
+ if (result.dropped?.length) {
10359
+ for (const col of result.dropped) {
10360
+ console.log(source_default.red(` Dropped: ${col}`));
10361
+ }
10362
+ }
10363
+ if (result.altered?.length) {
10364
+ for (const col of result.altered) {
10365
+ console.log(source_default.yellow(` Altered: ${col}`));
10366
+ }
10367
+ }
10368
+ } catch (err) {
10369
+ migrateSpinner.fail("Migration failed");
10370
+ console.error(source_default.red(` ${err.message}`));
10371
+ process.exit(1);
10372
+ }
10373
+ }
10374
+ function parseModelFields(content, fileName) {
10375
+ const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
10376
+ if (!modelMatch)
10377
+ return null;
10378
+ const fields = [];
10379
+ const fieldRegex = /(\w+)\s*:\s*field\.(\w+)\(\s*(?:\[([^\]]*)\])?\s*\)((?:\.\w+\([^)]*\))*)/g;
10380
+ let match;
10381
+ while ((match = fieldRegex.exec(content)) !== null) {
10382
+ const [, name, type] = match;
10383
+ fields.push({ name, type });
10384
+ }
10385
+ return { name: modelMatch[1], fields };
10386
+ }
10387
+
10225
10388
  // src/index.ts
10226
10389
  var VERSION = "1.0.1";
10227
10390
  var program2 = new Command;
@@ -10241,6 +10404,7 @@ var graph = program2.command("graph").description("Construct Graph — data mode
10241
10404
  graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
10242
10405
  graph.command("generate <model> [fields...]").alias("g").description("Generate a data model").option("--access <rules>", "Access rules (e.g. read:member,create:member,update:owner,delete:admin)").action((model, fields, opts) => generate2(model, fields, opts));
10243
10406
  graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
10407
+ graph.command("migrate").description("Compare local models with server schema and apply changes").option("--apply", "Apply destructive changes (drop columns, alter constraints)").action(async (opts) => graphMigrate(opts));
10244
10408
  var space = program2.command("space").description("Space development commands");
10245
10409
  space.command("scaffold [name]").alias("new").alias("create").option("--with-tests", "Include E2E testing boilerplate").action(async (name, opts) => scaffold(name, opts));
10246
10410
  space.command("build").option("--entry-only").action(async (opts) => build(opts));
@@ -7,7 +7,7 @@ export default defineConfig({
7
7
  build: {
8
8
  lib: {
9
9
  entry: resolve(__dirname, 'src/entry.ts'),
10
- name: 'space-{{.ID}}',
10
+ name: '__CONSTRUCT_SPACE_{{.IDUpper}}',
11
11
  fileName: 'space-{{.ID}}',
12
12
  formats: ['iife'],
13
13
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@construct-space/cli",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "Construct CLI — scaffold, build, develop, and publish spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,7 @@ export default defineConfig({
7
7
  build: {
8
8
  lib: {
9
9
  entry: resolve(__dirname, 'src/entry.ts'),
10
- name: 'space-{{.ID}}',
10
+ name: '__CONSTRUCT_SPACE_{{.IDUpper}}',
11
11
  fileName: 'space-{{.ID}}',
12
12
  formats: ['iife'],
13
13
  },