Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
435 changes: 129 additions & 306 deletions README.md

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions ntn-roblox.toml.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
# Generated by `ntn-roblox init`. Safe to commit — secrets belong in .env only.

[notion]
# Parent page where `ntn-roblox create-databases` creates the three databases
# Parent page where `ntn-roblox create-db` creates the databases
# (32-char hex ID from the page URL). Share this page with your integration first.
parent_page_id = ""
# Filled by `ntn-roblox create-databases`, or set manually if you create databases yourself.
# Filled by `ntn-roblox create-db`, or set manually if you create databases yourself.
dev_product_db_id = ""
game_pass_db_id = ""
badge_db_id = ""
asset_db_id = ""
# is_inline = true

[roblox]
# Use universe_id for a single universe, OR universes for multi-universe sync (not both).
universe_id = 1234567890
# universes = { main = 1234567890, staging = 9876543210 }
# badge_payment_source = "user"
# Required when asset_db_id is configured (for asset upload ownership):
# [roblox.asset_creator]
# is_group = false
# id = 12345678

[logging]
# level = "info"
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "notion-to-roblox",
"version": "0.1.1",
"version": "0.2.0",
"private": true,
"description": "CLI to sync Notion database records to Roblox Developer Products, Game Passes, and Badges via Open Cloud API",
"license": "MIT",
Expand All @@ -24,8 +24,10 @@
"check": "tsc --noEmit",
"test": "node --experimental-test-module-mocks --import tsx --import ./test/setup.ts --test test/**/*.test.ts",
"init": "tsx src/cli.ts init",
"create-db": "tsx src/cli.ts create-databases",
"create-databases": "tsx src/cli.ts create-databases",
"sync": "tsx src/cli.ts",
"sync": "tsx src/cli.ts sync",
"update": "tsx src/cli.ts update",
"start": "node dist/cli.js",
"compile": "bash scripts/package-release.sh"
},
Expand Down
2 changes: 1 addition & 1 deletion rokit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

[tools]
# After publishing a GitHub Release, consumer projects can add:
# ntn-roblox = "Zac134/NotionToRoblox@0.1.1"
# ntn-roblox = "Zac134/NotionToRoblox@0.2.0"
6 changes: 5 additions & 1 deletion scripts/package-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ trap 'rm -rf "$WORK_DIR"' EXIT
BINARY_PATH="$WORK_DIR/$BIN_NAME"

echo "Compiling src/cli.ts for $BUN_TARGET ..."
bun build src/cli.ts --compile --target="$BUN_TARGET" --outfile="$BINARY_PATH"
bun build src/cli.ts \
--compile \
--target="$BUN_TARGET" \
--define "NTN_ROBLOX_VERSION=\"$VERSION\"" \
--outfile="$BINARY_PATH"

if [[ "$BIN_NAME" != *.exe ]]; then
chmod +x "$BINARY_PATH"
Expand Down
13 changes: 13 additions & 0 deletions src/assetTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const SUPPORTED_ASSET_TYPES = [
"Animation",
"Audio",
"Decal",
"Image",
"Model",
] as const;

export type SupportedAssetType = (typeof SUPPORTED_ASSET_TYPES)[number];

export function isSupportedAssetType(value: string): value is SupportedAssetType {
return (SUPPORTED_ASSET_TYPES as readonly string[]).includes(value);
}
110 changes: 86 additions & 24 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import { pathToFileURL } from "node:url";
import {
loadCreateDatabasesConfig,
loadSyncConfig,
Expand All @@ -10,29 +11,34 @@ import { loadEnvFile } from "./env.js";
import { runInit } from "./init/runInit.js";
import type { ResourceType } from "./types.js";
import { setLogLevel } from "./util/logger.js";
import { VERSION } from "./version.js";

type Command = "sync" | "init" | "create-databases";
type SyncCommand = "sync" | "update";
type Command = SyncCommand | "init" | "create-databases" | "create-db";

interface SyncParsedArgs {
command: "sync";
command: SyncCommand;
dryRun: boolean;
reportOnly: boolean;
force: boolean;
typeFilter?: ResourceType;
targetFilter?: string;
help: boolean;
version: boolean;
}

interface InitParsedArgs {
command: "init";
force: boolean;
help: boolean;
version: boolean;
}

interface CreateDatabasesParsedArgs {
command: "create-databases";
command: "create-databases" | "create-db";
parentPageId?: string;
force: boolean;
help: boolean;
version: boolean;
}

type ParsedArgs =
Expand All @@ -44,41 +50,56 @@ const RESOURCE_TYPES: ResourceType[] = [
"developer-product",
"game-pass",
"badge",
"asset",
];

const COMMANDS: Command[] = ["sync", "init", "create-databases"];
const COMMANDS: Command[] = [
"sync",
"update",
"init",
"create-databases",
"create-db",
];

function printUsage(): void {
console.error(`Usage:
console.log(`Usage:
ntn-roblox init [options]
ntn-roblox create-db [options]
ntn-roblox create-databases [options]
ntn-roblox sync [options]
ntn-roblox update [options]

Development:
npm run init -- [options]
npm run create-databases -- [options]
npm run create-db -- [options]
npm run sync -- [options]
npm run update -- [options]

Commands:
init Create .env and ntn-roblox.toml in the current directory
create-db Alias for create-databases
create-databases Create Notion databases and write IDs to ntn-roblox.toml
sync Run full synchronization (default)
sync Create Roblox items for rows without Roblox ID
update Update Roblox items for rows with Roblox ID

Init options:
--force Overwrite existing .env / ntn-roblox.toml
--help, -h Show this help message
--version, -V Show version

Create-databases options:
--parent-page-id=<id> Notion parent page ID (overrides ntn-roblox.toml)
--force Create new databases even if IDs are already configured
--help, -h Show this help message
--version, -V Show version

Sync options:
Sync / update options:
--dry-run Log planned mutations without writing to Roblox or Notion
--report-only List Roblox orphans only; skip create/update
--force Re-sync rows with Sync Status = Synced
--type=<type> Limit to one resource type (${RESOURCE_TYPES.join(" | ")})
--target=<key> Limit to one universe key (multi-universe configs only)
--help, -h Show this help message
--version, -V Show version
`);
}

Expand All @@ -97,10 +118,6 @@ function parseSyncArg(arg: string, parsed: SyncParsedArgs): void {
parsed.reportOnly = true;
return;
}
if (arg === "--force") {
parsed.force = true;
return;
}
if (arg.startsWith("--type=")) {
const value = arg.slice("--type=".length) as ResourceType;
if (!RESOURCE_TYPES.includes(value)) {
Expand All @@ -111,6 +128,14 @@ function parseSyncArg(arg: string, parsed: SyncParsedArgs): void {
parsed.typeFilter = value;
return;
}
if (arg.startsWith("--target=")) {
const value = arg.slice("--target=".length).trim();
if (!value) {
throw new Error("Invalid --target value: value must not be empty");
}
parsed.targetFilter = value;
return;
}
throw new Error(`Unknown argument: ${arg}`);
}

Expand All @@ -119,6 +144,7 @@ function parseInitArg(arg: string, parsed: InitParsedArgs): void {
arg === "--dry-run" ||
arg === "--report-only" ||
arg.startsWith("--type=") ||
arg.startsWith("--target=") ||
arg.startsWith("--parent-page-id=") ||
arg === "--write-toml"
) {
Expand All @@ -139,6 +165,7 @@ function parseCreateDatabasesArg(
arg === "--dry-run" ||
arg === "--report-only" ||
arg.startsWith("--type=") ||
arg.startsWith("--target=") ||
arg === "--write-toml"
) {
throw new Error(`Unknown argument: ${arg}`);
Expand All @@ -164,9 +191,17 @@ function isCommand(value: string): value is Command {
return (COMMANDS as string[]).includes(value);
}

function normalizeCommand(command: Command): ParsedArgs["command"] {
if (command === "create-db") {
return "create-databases";
}
return command;
}

function parseArgs(argv: string[]): ParsedArgs {
let command: Command | undefined;
let help = false;
let version = false;

for (const arg of argv) {
if (isCommand(arg)) {
Expand All @@ -180,38 +215,44 @@ function parseArgs(argv: string[]): ParsedArgs {
const resolvedCommand = command ?? "sync";

const syncParsed: SyncParsedArgs = {
command: "sync",
command: resolvedCommand === "update" ? "update" : "sync",
dryRun: false,
reportOnly: false,
force: false,
help: false,
version: false,
};

const initParsed: InitParsedArgs = {
command: "init",
force: false,
help: false,
version: false,
};

const createDatabasesParsed: CreateDatabasesParsedArgs = {
command: "create-databases",
force: false,
help: false,
version: false,
};

for (const arg of argv) {
if (arg === "--help" || arg === "-h") {
help = true;
continue;
}
if (arg === "--version" || arg === "-V") {
version = true;
continue;
}
if (isCommand(arg)) {
continue;
}
if (!arg.startsWith("-")) {
throw new Error(`Unknown argument: ${arg}`);
}

if (resolvedCommand === "sync") {
if (resolvedCommand === "sync" || resolvedCommand === "update") {
parseSyncArg(arg, syncParsed);
} else if (resolvedCommand === "init") {
parseInitArg(arg, initParsed);
Expand All @@ -222,15 +263,18 @@ function parseArgs(argv: string[]): ParsedArgs {

if (resolvedCommand === "init") {
initParsed.help = help;
initParsed.version = version;
return initParsed;
}

if (resolvedCommand === "create-databases") {
if (resolvedCommand === "create-databases" || resolvedCommand === "create-db") {
createDatabasesParsed.help = help;
createDatabasesParsed.version = version;
return createDatabasesParsed;
}

syncParsed.help = help;
syncParsed.version = version;
return syncParsed;
}

Expand All @@ -239,6 +283,11 @@ async function main(): Promise<void> {

const args = parseArgs(process.argv.slice(2));

if (args.version) {
console.log(VERSION);
return;
}

if (args.help) {
printUsage();
return;
Expand All @@ -258,24 +307,37 @@ async function main(): Promise<void> {
return;
}

if (args.command !== "sync" && args.command !== "update") {
return;
}

setConfig(loadSyncConfig());

const { runSync, shouldExitWithError } = await import("./sync/engine.js");

const result = await runSync({
mode: args.command,
dryRun: args.dryRun,
reportOnly: args.reportOnly,
force: args.force,
typeFilter: args.typeFilter,
targetFilter: args.targetFilter,
});

if (shouldExitWithError(result)) {
process.exitCode = 1;
}
}

main().catch(async (error) => {
const { logger } = await import("./util/logger.js");
logger.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
const isMainModule =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href;

if (isMainModule) {
main().catch(async (error) => {
const { logger } = await import("./util/logger.js");
logger.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
}

export { parseArgs, printUsage, VERSION };
Loading
Loading