@construct-space/cli 1.0.2 → 1.0.4

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");
@@ -10084,7 +10102,7 @@ function updateBarrel(modelsDir, modelName) {
10084
10102
  }
10085
10103
 
10086
10104
  // src/commands/graph/push.ts
10087
- import { existsSync as existsSync16, readdirSync as readdirSync5, readFileSync as readFileSync12 } from "fs";
10105
+ import { existsSync as existsSync16, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "fs";
10088
10106
  import { join as join20, basename as basename7 } from "path";
10089
10107
  async function graphPush() {
10090
10108
  const root = process.cwd();
@@ -10098,7 +10116,7 @@ async function graphPush() {
10098
10116
  console.error(source_default.red("No src/models/ directory found. Run 'construct graph init' first."));
10099
10117
  process.exit(1);
10100
10118
  }
10101
- const modelFiles = readdirSync5(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10119
+ const modelFiles = readdirSync4(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10102
10120
  if (modelFiles.length === 0) {
10103
10121
  console.error(source_default.red("No model files found in src/models/"));
10104
10122
  console.log(source_default.dim(" Generate one: construct graph g User name:string email:string"));
@@ -10222,6 +10240,150 @@ function parseModelFile(content, fileName) {
10222
10240
  return result;
10223
10241
  }
10224
10242
 
10243
+ // src/commands/graph/migrate.ts
10244
+ import { existsSync as existsSync17, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "fs";
10245
+ import { join as join21, basename as basename8 } from "path";
10246
+ async function graphMigrate(options) {
10247
+ const root = process.cwd();
10248
+ if (!exists(root)) {
10249
+ console.error(source_default.red("No space.manifest.json found in current directory"));
10250
+ process.exit(1);
10251
+ }
10252
+ const m = read(root);
10253
+ const modelsDir = join21(root, "src", "models");
10254
+ if (!existsSync17(modelsDir)) {
10255
+ console.error(source_default.red("No src/models/ directory. Run 'construct graph init' first."));
10256
+ process.exit(1);
10257
+ }
10258
+ let creds;
10259
+ try {
10260
+ creds = load2();
10261
+ } catch (err) {
10262
+ console.error(source_default.red(err.message));
10263
+ process.exit(1);
10264
+ }
10265
+ const graphURL = process.env.GRAPH_URL || "https://graph.construct.space";
10266
+ const spinner = ora("Fetching current schema...").start();
10267
+ let serverModels = [];
10268
+ try {
10269
+ const resp = await fetch(`${graphURL}/api/schemas/${m.id}`, {
10270
+ headers: { Authorization: `Bearer ${creds.token}` }
10271
+ });
10272
+ if (resp.ok) {
10273
+ const data = await resp.json();
10274
+ serverModels = data.models || [];
10275
+ }
10276
+ spinner.succeed("Schema fetched");
10277
+ } catch {
10278
+ spinner.fail("Could not fetch schema");
10279
+ process.exit(1);
10280
+ }
10281
+ const modelFiles = readdirSync5(modelsDir).filter((f) => f.endsWith(".ts") && f !== "index.ts");
10282
+ const localModels = [];
10283
+ for (const file of modelFiles) {
10284
+ const content = readFileSync13(join21(modelsDir, file), "utf-8");
10285
+ const model = parseModelFields(content, basename8(file, ".ts"));
10286
+ if (model)
10287
+ localModels.push(model);
10288
+ }
10289
+ console.log();
10290
+ let hasChanges = false;
10291
+ for (const server of serverModels) {
10292
+ const local = localModels.find((m2) => m2.name === server.name);
10293
+ const serverFields = (server.fields || []).map((f) => f.name);
10294
+ if (!local) {
10295
+ console.log(source_default.yellow(` Model "${server.name}" exists on server but not locally`));
10296
+ hasChanges = true;
10297
+ continue;
10298
+ }
10299
+ const localFields = local.fields.map((f) => f.name);
10300
+ for (const sf of serverFields) {
10301
+ if (!localFields.includes(sf)) {
10302
+ console.log(source_default.red(` - ${server.name}.${sf}`), source_default.dim("(on server, not in local model — can drop)"));
10303
+ hasChanges = true;
10304
+ }
10305
+ }
10306
+ for (const lf of localFields) {
10307
+ if (!serverFields.includes(lf)) {
10308
+ console.log(source_default.green(` + ${server.name}.${lf}`), source_default.dim("(new — will be added on push)"));
10309
+ hasChanges = true;
10310
+ }
10311
+ }
10312
+ }
10313
+ for (const local of localModels) {
10314
+ if (!serverModels.find((m2) => m2.name === local.name)) {
10315
+ console.log(source_default.green(` + Model "${local.name}"`), source_default.dim("(new — will be created on push)"));
10316
+ hasChanges = true;
10317
+ }
10318
+ }
10319
+ if (!hasChanges) {
10320
+ console.log(source_default.green(" Schema is in sync — no changes needed"));
10321
+ return;
10322
+ }
10323
+ console.log();
10324
+ if (!options?.apply) {
10325
+ console.log(source_default.dim(" Run with --apply to apply destructive changes"));
10326
+ console.log(source_default.dim(' Or run "construct graph push" to add new fields/models'));
10327
+ return;
10328
+ }
10329
+ const proceed = await dist_default4({ message: "Apply destructive schema changes? This cannot be undone." });
10330
+ if (!proceed) {
10331
+ console.log("Cancelled.");
10332
+ return;
10333
+ }
10334
+ const migrateSpinner = ora("Applying migrations...").start();
10335
+ try {
10336
+ const resp = await fetch(`${graphURL}/api/schemas/migrate`, {
10337
+ method: "POST",
10338
+ headers: {
10339
+ "Content-Type": "application/json",
10340
+ Authorization: `Bearer ${creds.token}`,
10341
+ "X-Space-ID": m.id
10342
+ },
10343
+ body: JSON.stringify({
10344
+ space_id: m.id,
10345
+ project_id: "default",
10346
+ local_models: localModels
10347
+ })
10348
+ });
10349
+ if (!resp.ok) {
10350
+ const body = await resp.text();
10351
+ migrateSpinner.fail("Migration failed");
10352
+ console.error(source_default.red(` ${resp.status}: ${body}`));
10353
+ process.exit(1);
10354
+ }
10355
+ const result = await resp.json();
10356
+ migrateSpinner.succeed("Migrations applied");
10357
+ if (result.dropped?.length) {
10358
+ for (const col of result.dropped) {
10359
+ console.log(source_default.red(` Dropped: ${col}`));
10360
+ }
10361
+ }
10362
+ if (result.altered?.length) {
10363
+ for (const col of result.altered) {
10364
+ console.log(source_default.yellow(` Altered: ${col}`));
10365
+ }
10366
+ }
10367
+ } catch (err) {
10368
+ migrateSpinner.fail("Migration failed");
10369
+ console.error(source_default.red(` ${err.message}`));
10370
+ process.exit(1);
10371
+ }
10372
+ }
10373
+ function parseModelFields(content, fileName) {
10374
+ const modelMatch = content.match(/defineModel\s*\(\s*['"](\w+)['"]/);
10375
+ if (!modelMatch)
10376
+ return null;
10377
+ const fields = [];
10378
+ const fieldRegex = /(\w+)\s*:\s*field\.(\w+)\(\s*(?:\[([^\]]*)\])?\s*\)((?:\.\w+\([^)]*\))*)/g;
10379
+ let match;
10380
+ while ((match = fieldRegex.exec(content)) !== null) {
10381
+ const [, name, type] = match;
10382
+ fields.push({ name, type });
10383
+ }
10384
+ return { name: modelMatch[1], fields };
10385
+ }
10386
+
10225
10387
  // src/index.ts
10226
10388
  var VERSION = "1.0.1";
10227
10389
  var program2 = new Command;
@@ -10241,6 +10403,7 @@ var graph = program2.command("graph").description("Construct Graph — data mode
10241
10403
  graph.command("init").description("Initialize Graph in a space project").action(() => graphInit());
10242
10404
  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
10405
  graph.command("push").description("Register models with the Graph service").action(async () => graphPush());
10406
+ 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
10407
  var space = program2.command("space").description("Space development commands");
10245
10408
  space.command("scaffold [name]").alias("new").alias("create").option("--with-tests", "Include E2E testing boilerplate").action(async (name, opts) => scaffold(name, opts));
10246
10409
  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.4",
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
  },