@craftrpgs/cli 0.1.2 → 0.1.3

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.
Files changed (2) hide show
  1. package/dist/bin.js +137 -29
  2. package/package.json +2 -2
package/dist/bin.js CHANGED
@@ -27454,6 +27454,7 @@ function cliPushResponseForOpsSchema(ops) {
27454
27454
  }
27455
27455
  });
27456
27456
  }
27457
+ var MAX_IMPORT_ZIP_BYTES = 50 * 1024 * 1024;
27457
27458
  var cliImportOptionsSchema = exports_external.object({
27458
27459
  dryRun: exports_external.boolean().default(false),
27459
27460
  overwrite: exports_external.boolean().default(false),
@@ -41858,7 +41859,7 @@ function describeRemoteSource(resolved) {
41858
41859
  // package.json
41859
41860
  var package_default = {
41860
41861
  name: "@craftrpgs/cli",
41861
- version: "0.1.2",
41862
+ version: "0.1.3",
41862
41863
  description: "Sync Craft projects with a local folder — clone, edit with any coding agent, push back.",
41863
41864
  license: "UNLICENSED",
41864
41865
  homepage: "https://craftrpgs.com",
@@ -42280,6 +42281,36 @@ async function defaultCloneDir(base) {
42280
42281
  }
42281
42282
  throw new CliError(`Could not find a free directory name near ${base}; pass [dir] explicitly.`, 2);
42282
42283
  }
42284
+ async function assertUsableCloneTarget({
42285
+ targetDir,
42286
+ zipFilePaths
42287
+ }) {
42288
+ if (!existsSync4(targetDir)) {
42289
+ return;
42290
+ }
42291
+ const existing = await readdir3(targetDir);
42292
+ const visible = existing.filter((name) => !shouldSkipWorkspaceName(name));
42293
+ const collisions = zipFilePaths.filter((relative2) => {
42294
+ try {
42295
+ return existsSync4(joinUnderWorkspaceRoot(targetDir, relative2));
42296
+ } catch {
42297
+ return false;
42298
+ }
42299
+ });
42300
+ if (visible.length === 0 && collisions.length === 0) {
42301
+ return;
42302
+ }
42303
+ throw new CliError([
42304
+ `Refusing to clone into ${targetDir}:`,
42305
+ ...visible.map((name) => ` ${name} — pre-existing and would be mixed into the new workspace`),
42306
+ ...collisions.map((relative2) => ` ${relative2} — the project snapshot would overwrite it`),
42307
+ "Entries the workspace scanner ignores (.git/, .gitignore, other dotfiles) may pre-exist; move the listed items or pick a different [dir]."
42308
+ ].join(`
42309
+ `), 2);
42310
+ }
42311
+ function nextStepHint(targetDir) {
42312
+ return targetDir === process.cwd() ? "Next: run `craft status`." : "Next: cd in and run `craft status`.";
42313
+ }
42283
42314
  async function downloadAndUnpackSnapshot({
42284
42315
  client,
42285
42316
  projectId,
@@ -42290,12 +42321,8 @@ async function downloadAndUnpackSnapshot({
42290
42321
  const paths = Object.keys(entries);
42291
42322
  const rootDir = zipRootDir(paths);
42292
42323
  const targetDir = dirArg ? resolve4(dirArg) : await defaultCloneDir(resolve4(rootDir));
42293
- if (existsSync4(targetDir)) {
42294
- const existing = await readdir3(targetDir);
42295
- if (existing.length > 0) {
42296
- throw new CliError(`Refusing to clone into non-empty directory ${targetDir}`, 2);
42297
- }
42298
- }
42324
+ const zipFilePaths = paths.filter((path) => !path.endsWith("/")).map((path) => path.slice(rootDir.length + 1)).filter((relative2) => relative2 !== "");
42325
+ await assertUsableCloneTarget({ targetDir, zipFilePaths });
42299
42326
  await mkdir3(targetDir, { recursive: true });
42300
42327
  let fileCount = 0;
42301
42328
  for (const [path, bytes] of Object.entries(entries)) {
@@ -42362,12 +42389,15 @@ async function runClone(argv) {
42362
42389
  } else {
42363
42390
  printLines([
42364
42391
  `Cloned project ${target.projectId} into ${targetDir} (${fileCount} files).`,
42365
- "Next: cd in and run `craft status`."
42392
+ nextStepHint(targetDir)
42366
42393
  ]);
42367
42394
  }
42368
42395
  return 0;
42369
42396
  }
42370
42397
 
42398
+ // src/commands/create.ts
42399
+ import { basename } from "node:path";
42400
+
42371
42401
  // src/commands/clone.ts
42372
42402
  import { existsSync as existsSync5 } from "node:fs";
42373
42403
  import { mkdir as mkdir4, readdir as readdir4, writeFile as writeFile4 } from "node:fs/promises";
@@ -42401,6 +42431,36 @@ async function defaultCloneDir2(base) {
42401
42431
  }
42402
42432
  throw new CliError(`Could not find a free directory name near ${base}; pass [dir] explicitly.`, 2);
42403
42433
  }
42434
+ async function assertUsableCloneTarget2({
42435
+ targetDir,
42436
+ zipFilePaths
42437
+ }) {
42438
+ if (!existsSync5(targetDir)) {
42439
+ return;
42440
+ }
42441
+ const existing = await readdir4(targetDir);
42442
+ const visible = existing.filter((name) => !shouldSkipWorkspaceName(name));
42443
+ const collisions = zipFilePaths.filter((relative2) => {
42444
+ try {
42445
+ return existsSync5(joinUnderWorkspaceRoot(targetDir, relative2));
42446
+ } catch {
42447
+ return false;
42448
+ }
42449
+ });
42450
+ if (visible.length === 0 && collisions.length === 0) {
42451
+ return;
42452
+ }
42453
+ throw new CliError([
42454
+ `Refusing to clone into ${targetDir}:`,
42455
+ ...visible.map((name) => ` ${name} — pre-existing and would be mixed into the new workspace`),
42456
+ ...collisions.map((relative2) => ` ${relative2} — the project snapshot would overwrite it`),
42457
+ "Entries the workspace scanner ignores (.git/, .gitignore, other dotfiles) may pre-exist; move the listed items or pick a different [dir]."
42458
+ ].join(`
42459
+ `), 2);
42460
+ }
42461
+ function nextStepHint2(targetDir) {
42462
+ return targetDir === process.cwd() ? "Next: run `craft status`." : "Next: cd in and run `craft status`.";
42463
+ }
42404
42464
  async function downloadAndUnpackSnapshot2({
42405
42465
  client,
42406
42466
  projectId,
@@ -42411,12 +42471,8 @@ async function downloadAndUnpackSnapshot2({
42411
42471
  const paths = Object.keys(entries);
42412
42472
  const rootDir = zipRootDir2(paths);
42413
42473
  const targetDir = dirArg ? resolve5(dirArg) : await defaultCloneDir2(resolve5(rootDir));
42414
- if (existsSync5(targetDir)) {
42415
- const existing = await readdir4(targetDir);
42416
- if (existing.length > 0) {
42417
- throw new CliError(`Refusing to clone into non-empty directory ${targetDir}`, 2);
42418
- }
42419
- }
42474
+ const zipFilePaths = paths.filter((path) => !path.endsWith("/")).map((path) => path.slice(rootDir.length + 1)).filter((relative2) => relative2 !== "");
42475
+ await assertUsableCloneTarget2({ targetDir, zipFilePaths });
42420
42476
  await mkdir4(targetDir, { recursive: true });
42421
42477
  let fileCount = 0;
42422
42478
  for (const [path, bytes] of Object.entries(entries)) {
@@ -42444,6 +42500,10 @@ async function downloadAndUnpackSnapshot2({
42444
42500
  }
42445
42501
 
42446
42502
  // src/commands/create.ts
42503
+ var DIRECTORY_NAME_SEPARATORS = /[-_\s]+/;
42504
+ function projectNameFromDirectory(dirName) {
42505
+ return dirName.split(DIRECTORY_NAME_SEPARATORS).filter((word) => word.length > 0).map((word) => (word[0]?.toUpperCase() ?? "") + word.slice(1)).join(" ");
42506
+ }
42447
42507
  async function runCreate(argv) {
42448
42508
  const args = parseCommandArgs({
42449
42509
  argv,
@@ -42455,10 +42515,19 @@ async function runCreate(argv) {
42455
42515
  },
42456
42516
  maxPositionals: 2
42457
42517
  });
42458
- const [name, dirArg] = args.positionals;
42518
+ const [nameArg, dirPositional] = args.positionals;
42519
+ if (!nameArg) {
42520
+ throw new CliError('Usage: craft create <name> [dir] — quote names with spaces, e.g. craft create "My World" (or `craft create .` to create here, named after this directory)', 2);
42521
+ }
42522
+ if (nameArg === "." && dirPositional !== undefined) {
42523
+ throw new CliError('`craft create .` takes no [dir] — "." already means "create in the current directory, named after it". To pick the name yourself: craft create "My Name" .', 2);
42524
+ }
42525
+ const namedAfterCwd = nameArg === ".";
42526
+ const name = namedAfterCwd ? projectNameFromDirectory(basename(process.cwd())) : nameArg;
42459
42527
  if (!name) {
42460
- throw new CliError('Usage: craft create <name> [dir] quote names with spaces, e.g. craft create "My World"', 2);
42528
+ throw new CliError('Could not derive a project name from the current directory pass one explicitly: craft create "My World" .', 2);
42461
42529
  }
42530
+ const dirArg = namedAfterCwd ? "." : dirPositional;
42462
42531
  const resolved = resolveRemote({ flagRemote: stringFlag(args, "remote") });
42463
42532
  const remote = resolved.remote;
42464
42533
  const client = createCliClient({
@@ -42485,6 +42554,7 @@ async function runCreate(argv) {
42485
42554
  printJson({
42486
42555
  projectId: created.projectId,
42487
42556
  containerId: created.containerId,
42557
+ name,
42488
42558
  remote,
42489
42559
  dir: targetDir,
42490
42560
  files: fileCount
@@ -42492,8 +42562,11 @@ async function runCreate(argv) {
42492
42562
  } else {
42493
42563
  printLines([
42494
42564
  `Created project "${name}" (${created.projectId}) on ${remote} ` + `(${describeRemoteSource(resolved)}).`,
42565
+ ...namedAfterCwd ? [
42566
+ 'Named after the current directory — `craft create "My Name" .` picks the name yourself.'
42567
+ ] : [],
42495
42568
  `Cloned into ${targetDir} (${fileCount} files).`,
42496
- "Next: cd in and run `craft status`."
42569
+ nextStepHint2(targetDir)
42497
42570
  ]);
42498
42571
  }
42499
42572
  return 0;
@@ -43495,7 +43568,7 @@ async function workspacePin() {
43495
43568
 
43496
43569
  // src/commands/image.ts
43497
43570
  import { readFile as readFile6 } from "node:fs/promises";
43498
- import { basename, isAbsolute as isAbsolute2 } from "node:path";
43571
+ import { basename as basename2, isAbsolute as isAbsolute2 } from "node:path";
43499
43572
 
43500
43573
  // ../shared/src/ledger-constants.ts
43501
43574
  var SYSTEM_ACCOUNTS = {
@@ -43753,7 +43826,7 @@ async function runImageUpload(argv) {
43753
43826
  })
43754
43827
  });
43755
43828
  const form = new FormData;
43756
- form.append("file", new File([new Uint8Array(bytes)], basename(filePath)));
43829
+ form.append("file", new File([new Uint8Array(bytes)], basename2(filePath)));
43757
43830
  let result;
43758
43831
  try {
43759
43832
  result = await client.uploadImage({
@@ -43768,7 +43841,7 @@ async function runImageUpload(argv) {
43768
43841
  return 0;
43769
43842
  }
43770
43843
  printLines([
43771
- `Uploaded ${basename(filePath)} (${result.width}x${result.height}, ${Math.round(result.bytes / 1024)}KB as webp):`,
43844
+ `Uploaded ${basename2(filePath)} (${result.width}x${result.height}, ${Math.round(result.bytes / 1024)}KB as webp):`,
43772
43845
  ` ${result.image.url ?? "<no url returned>"}`,
43773
43846
  "",
43774
43847
  `To use it, set this object as the file's "image" field and push:`,
@@ -43850,7 +43923,7 @@ async function runImageGenerate(argv) {
43850
43923
  const referenceImageUrls = [];
43851
43924
  for (const reference of localReferenceImages) {
43852
43925
  const form = new FormData;
43853
- form.append("file", new File([new Uint8Array(reference.bytes)], basename(reference.path)));
43926
+ form.append("file", new File([new Uint8Array(reference.bytes)], basename2(reference.path)));
43854
43927
  let uploaded;
43855
43928
  try {
43856
43929
  uploaded = await client.uploadImage({
@@ -43953,7 +44026,7 @@ function toActionableError(error48, operation = "generation") {
43953
44026
 
43954
44027
  // src/commands/import.ts
43955
44028
  import { readFile as readFile7 } from "node:fs/promises";
43956
- import { basename as basename2 } from "node:path";
44029
+ import { basename as basename3 } from "node:path";
43957
44030
  async function runImport(argv) {
43958
44031
  const args = parseCommandArgs({
43959
44032
  argv,
@@ -43986,7 +44059,7 @@ async function runImport(argv) {
43986
44059
  throw new CliError(`Could not read "${zipPath}": ${error48 instanceof Error ? error48.message : String(error48)}`, 2);
43987
44060
  }
43988
44061
  const form = new FormData;
43989
- form.append("file", new File([new Uint8Array(zipBytes)], basename2(zipPath)));
44062
+ form.append("file", new File([new Uint8Array(zipBytes)], basename3(zipPath)));
43990
44063
  if (args.booleans.has("dry-run")) {
43991
44064
  form.append("dryRun", "true");
43992
44065
  }
@@ -49800,8 +49873,11 @@ var AUTH_COMMAND_LINES_DEV = ` login [--remote <env|origin>] Sign in with your
49800
49873
  Show or set the environment used from this directory
49801
49874
  `;
49802
49875
  var HELP_COMMAND_LINES = ` clone <projectIdOrUrl> [dir] Download a project as a local workspace
49876
+ ([dir] may be "." for the current directory)
49803
49877
  create <name> [dir] [--description "..."] [--empty]
49804
- Create a new project and clone it locally
49878
+ Create a new project and clone it locally:
49879
+ craft create "My World" . \u2192 current directory
49880
+ craft create . \u2192 here, named after the dir
49805
49881
  (--empty skips the starter file types)
49806
49882
  projects List the projects your account owns
49807
49883
  log <path> [--limit <n>] Show a file's version history (newest first)
@@ -49882,12 +49958,28 @@ Sign out \u2014 forget the credentials stored on this machine.
49882
49958
  clone: `Usage: craft clone <projectIdOrUrl> [dir]
49883
49959
 
49884
49960
  Download a project as a local workspace. Accepts a project id or a pasted
49885
- project URL. [dir] defaults to a folder named after the project.
49961
+ project URL. [dir] defaults to a new folder named after the project; pass
49962
+ "." (or any path) to choose the destination. The target may already hold
49963
+ entries the workspace ignores (.git/, .gitignore, other dotfiles), but
49964
+ nothing the snapshot would mix with or overwrite.
49965
+
49966
+ Examples:
49967
+ craft clone <id> # into ./<project-name>/
49968
+ craft clone <id> my-dir # into ./my-dir/
49969
+ craft clone <id> . # into the current directory
49886
49970
  `,
49887
49971
  create: `Usage: craft create <name> [dir] [--description "..."] [--empty]
49972
+ craft create . (create here, named after the current directory)
49888
49973
 
49889
49974
  Create a new project on the server and clone it into [dir] (defaults to a
49890
- folder named after the project).
49975
+ new folder named after the project; "." uses the current directory \u2014
49976
+ pre-existing dotfiles like .git/ are fine).
49977
+
49978
+ Examples:
49979
+ craft create "My World" # creates ./my-world/
49980
+ craft create "My World" my-dir # creates ./my-dir/
49981
+ craft create "My World" . # into the current directory
49982
+ craft create . # here, named after the directory
49891
49983
 
49892
49984
  By default the project seeds the standard starter file types (Character,
49893
49985
  Location, Equipment, Game Start, GM Instructions) plus example files \u2014
@@ -50155,13 +50247,29 @@ A cloned workspace always syncs with the server it was cloned from.
50155
50247
 
50156
50248
  Download a project as a local workspace. A full project URL carries its
50157
50249
  origin; a bare project id uses the current environment (see \`craft help
50158
- env\`). [dir] defaults to a folder named after the project.
50250
+ env\`). [dir] defaults to a new folder named after the project; pass "."
50251
+ (or any path) to choose the destination. The target may already hold
50252
+ entries the workspace ignores (.git/, .gitignore, other dotfiles), but
50253
+ nothing the snapshot would mix with or overwrite.
50254
+
50255
+ Examples:
50256
+ craft clone <id> # into ./<project-name>/
50257
+ craft clone <id> my-dir # into ./my-dir/
50258
+ craft clone <id> . # into the current directory
50159
50259
  `,
50160
50260
  create: `Usage: craft create <name> [dir] [--description "..."] [--empty]
50261
+ craft create . (create here, named after the current directory)
50161
50262
 
50162
50263
  Create a new project on the server and clone it into [dir] (defaults to a
50163
- folder named after the project). Targets the current environment \u2014 check
50164
- with \`craft env\`.
50264
+ new folder named after the project; "." uses the current directory \u2014
50265
+ pre-existing dotfiles like .git/ are fine). Targets the current
50266
+ environment \u2014 check with \`craft env\`.
50267
+
50268
+ Examples:
50269
+ craft create "My World" # creates ./my-world/
50270
+ craft create "My World" my-dir # creates ./my-dir/
50271
+ craft create "My World" . # into the current directory
50272
+ craft create . # here, named after the directory
50165
50273
 
50166
50274
  By default the project seeds the standard starter file types (Character,
50167
50275
  Location, Equipment, Game Start, GM Instructions) plus example files \u2014
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@craftrpgs/cli",
3
- "version": "0.1.2",
4
- "description": "Sync Craft projects with a local folder \u2014 clone, edit with any coding agent, push back.",
3
+ "version": "0.1.3",
4
+ "description": "Sync Craft projects with a local folder clone, edit with any coding agent, push back.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://craftrpgs.com",
7
7
  "type": "module",