rowork 0.1.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/LICENSE +21 -0
- package/README.md +95 -0
- package/bin/rowork.js +18 -0
- package/dashboard/app.css +234 -0
- package/dashboard/app.js +117 -0
- package/dashboard/index.html +67 -0
- package/dist/cli/errors.d.ts +15 -0
- package/dist/cli/errors.js +18 -0
- package/dist/cli/program.d.ts +19 -0
- package/dist/cli/program.js +59 -0
- package/dist/cli/registry.d.ts +37 -0
- package/dist/cli/registry.js +61 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.js +77 -0
- package/dist/commands/assets-setup.d.ts +18 -0
- package/dist/commands/assets-setup.js +136 -0
- package/dist/commands/assets.d.ts +2 -0
- package/dist/commands/assets.js +153 -0
- package/dist/commands/console.d.ts +4 -0
- package/dist/commands/console.js +141 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +50 -0
- package/dist/commands/dev-background.d.ts +3 -0
- package/dist/commands/dev-background.js +87 -0
- package/dist/commands/dev.d.ts +2 -0
- package/dist/commands/dev.js +270 -0
- package/dist/commands/eject.d.ts +2 -0
- package/dist/commands/eject.js +136 -0
- package/dist/commands/index.d.ts +8 -0
- package/dist/commands/index.js +54 -0
- package/dist/commands/info.d.ts +3 -0
- package/dist/commands/info.js +50 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +55 -0
- package/dist/commands/links.d.ts +32 -0
- package/dist/commands/links.js +152 -0
- package/dist/commands/make-event.d.ts +2 -0
- package/dist/commands/make-event.js +160 -0
- package/dist/commands/make-menu.d.ts +3 -0
- package/dist/commands/make-menu.js +36 -0
- package/dist/commands/make-stat.d.ts +2 -0
- package/dist/commands/make-stat.js +199 -0
- package/dist/commands/make-tool.d.ts +2 -0
- package/dist/commands/make-tool.js +124 -0
- package/dist/commands/make-ui.d.ts +3 -0
- package/dist/commands/make-ui.js +199 -0
- package/dist/commands/make.d.ts +21 -0
- package/dist/commands/make.js +181 -0
- package/dist/commands/next-steps.d.ts +5 -0
- package/dist/commands/next-steps.js +18 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +144 -0
- package/dist/commands/studio.d.ts +3 -0
- package/dist/commands/studio.js +45 -0
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +173 -0
- package/dist/commands/wire.d.ts +2 -0
- package/dist/commands/wire.js +34 -0
- package/dist/core/agent-docs.d.ts +23 -0
- package/dist/core/agent-docs.js +151 -0
- package/dist/core/assets.d.ts +96 -0
- package/dist/core/assets.js +268 -0
- package/dist/core/background.d.ts +47 -0
- package/dist/core/background.js +149 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +62 -0
- package/dist/core/exec.d.ts +22 -0
- package/dist/core/exec.js +38 -0
- package/dist/core/format-generated.d.ts +12 -0
- package/dist/core/format-generated.js +28 -0
- package/dist/core/generate.d.ts +37 -0
- package/dist/core/generate.js +94 -0
- package/dist/core/github-release.d.ts +28 -0
- package/dist/core/github-release.js +38 -0
- package/dist/core/info.d.ts +77 -0
- package/dist/core/info.js +65 -0
- package/dist/core/modules.d.ts +32 -0
- package/dist/core/modules.js +265 -0
- package/dist/core/naming.d.ts +6 -0
- package/dist/core/naming.js +26 -0
- package/dist/core/open-cloud.d.ts +58 -0
- package/dist/core/open-cloud.js +170 -0
- package/dist/core/project-index.d.ts +26 -0
- package/dist/core/project-index.js +59 -0
- package/dist/core/rojo-edit.d.ts +18 -0
- package/dist/core/rojo-edit.js +53 -0
- package/dist/core/rokit-installer.d.ts +15 -0
- package/dist/core/rokit-installer.js +77 -0
- package/dist/core/scaffold.d.ts +44 -0
- package/dist/core/scaffold.js +163 -0
- package/dist/core/schema-edit.d.ts +46 -0
- package/dist/core/schema-edit.js +125 -0
- package/dist/core/source-scan.d.ts +24 -0
- package/dist/core/source-scan.js +84 -0
- package/dist/core/studio.d.ts +83 -0
- package/dist/core/studio.js +225 -0
- package/dist/core/toolchain.d.ts +31 -0
- package/dist/core/toolchain.js +98 -0
- package/dist/core/tsconfig-edit.d.ts +11 -0
- package/dist/core/tsconfig-edit.js +47 -0
- package/dist/core/ui-edit.d.ts +31 -0
- package/dist/core/ui-edit.js +89 -0
- package/dist/core/versions.d.ts +24 -0
- package/dist/core/versions.js +67 -0
- package/dist/dashboard/open.d.ts +3 -0
- package/dist/dashboard/open.js +18 -0
- package/dist/dashboard/server.d.ts +28 -0
- package/dist/dashboard/server.js +191 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/main.d.ts +9 -0
- package/dist/main.js +135 -0
- package/dist/modules/format.d.ts +3 -0
- package/dist/modules/format.js +28 -0
- package/dist/modules/index.d.ts +7 -0
- package/dist/modules/index.js +12 -0
- package/dist/modules/integrations.d.ts +8 -0
- package/dist/modules/integrations.js +54 -0
- package/dist/modules/leaderstats.d.ts +3 -0
- package/dist/modules/leaderstats.js +104 -0
- package/dist/modules/lint.d.ts +5 -0
- package/dist/modules/lint.js +43 -0
- package/dist/modules/networking.d.ts +23 -0
- package/dist/modules/networking.js +246 -0
- package/dist/modules/player-data.d.ts +19 -0
- package/dist/modules/player-data.js +176 -0
- package/dist/modules/types.d.ts +96 -0
- package/dist/modules/types.js +2 -0
- package/dist/modules/ui.d.ts +3 -0
- package/dist/modules/ui.js +67 -0
- package/dist/plugins/api.d.ts +127 -0
- package/dist/plugins/api.js +21 -0
- package/dist/plugins/loader.d.ts +20 -0
- package/dist/plugins/loader.js +128 -0
- package/dist/process/log-mux.d.ts +13 -0
- package/dist/process/log-mux.js +31 -0
- package/dist/process/supervisor.d.ts +48 -0
- package/dist/process/supervisor.js +141 -0
- package/dist/process/tree-kill.d.ts +25 -0
- package/dist/process/tree-kill.js +70 -0
- package/dist/templates/engine.d.ts +18 -0
- package/dist/templates/engine.js +54 -0
- package/dist/ui/logger.d.ts +5 -0
- package/dist/ui/logger.js +53 -0
- package/dist/ui/prompt.d.ts +9 -0
- package/dist/ui/prompt.js +24 -0
- package/package.json +69 -0
- package/templates/init/README.md.tmpl +37 -0
- package/templates/init/_gitignore.tmpl +23 -0
- package/templates/init/default.project.json.tmpl +57 -0
- package/templates/init/package.json.tmpl +12 -0
- package/templates/init/rokit.toml.tmpl +5 -0
- package/templates/init/src/client/controllers/ExampleController.ts.tmpl +12 -0
- package/templates/init/src/client/runtime.client.ts.tmpl +6 -0
- package/templates/init/src/server/runtime.server.ts.tmpl +12 -0
- package/templates/init/src/server/services/ExampleService.ts.tmpl +12 -0
- package/templates/init/src/shared/_gitkeep.tmpl +0 -0
- package/templates/init/tsconfig.json.tmpl +28 -0
- package/templates/integrations/data-replication/DataReplicationService.ts.tmpl +33 -0
- package/templates/integrations/data-replication/PlayerDataController.ts.tmpl +40 -0
- package/templates/integrations/data-replication/dataEvents.ts.tmpl +21 -0
- package/templates/make/component.ts.tmpl +8 -0
- package/templates/make/controller.ts.tmpl +6 -0
- package/templates/make/event-handler.ts.tmpl +22 -0
- package/templates/make/screen.tsx.tmpl +35 -0
- package/templates/make/service.ts.tmpl +6 -0
- package/templates/make/stat-service.ts.tmpl +26 -0
- package/templates/make/ui-button.tsx.tmpl +22 -0
- package/templates/make/ui-image.tsx.tmpl +11 -0
- package/templates/make/ui-label.tsx.tmpl +18 -0
- package/templates/make/ui-panel.tsx.tmpl +33 -0
- package/templates/make/ui-story.tsx.tmpl +13 -0
- package/templates/modules/format/prettierignore.tmpl +9 -0
- package/templates/modules/format/prettierrc.json.tmpl +4 -0
- package/templates/modules/leaderstats/LeaderstatsService.ts.tmpl +69 -0
- package/templates/modules/lint/eslint.config.mjs.tmpl +17 -0
- package/templates/modules/networking/client-network.ts.tmpl +9 -0
- package/templates/modules/networking/networking.ts.tmpl +22 -0
- package/templates/modules/networking/rate-limit.ts.tmpl +52 -0
- package/templates/modules/networking/server-network.ts.tmpl +20 -0
- package/templates/modules/player-data/PlayerData.ts.tmpl +15 -0
- package/templates/modules/player-data/PlayerDataService.ts.tmpl +127 -0
- package/templates/modules/ui/App.tsx.tmpl +19 -0
- package/templates/modules/ui/Button.story.tsx.tmpl +13 -0
- package/templates/modules/ui/Button.tsx.tmpl +21 -0
- package/templates/modules/ui/UiController.tsx.tmpl +21 -0
- package/templates/modules/ui/screens/HomeScreen.tsx.tmpl +20 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CommandDefinition } from "../plugins/api.js";
|
|
2
|
+
/** Where a command came from, used to arbitrate name conflicts. */
|
|
3
|
+
export type CommandOrigin = {
|
|
4
|
+
kind: "core";
|
|
5
|
+
} | {
|
|
6
|
+
kind: "plugin";
|
|
7
|
+
pluginName: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "local";
|
|
10
|
+
file: string;
|
|
11
|
+
};
|
|
12
|
+
export interface RegisteredCommand {
|
|
13
|
+
definition: CommandDefinition;
|
|
14
|
+
origin: CommandOrigin;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Registry of every command, whatever its source.
|
|
18
|
+
*
|
|
19
|
+
* Conflict arbitration: core always wins. Without this rule a third-party
|
|
20
|
+
* plugin could silently hijack `rowork init`. Between plugins, first
|
|
21
|
+
* registration wins and the conflict is reported.
|
|
22
|
+
*/
|
|
23
|
+
export declare class CommandRegistry {
|
|
24
|
+
private readonly commands;
|
|
25
|
+
private readonly aliases;
|
|
26
|
+
register(definition: CommandDefinition, origin: CommandOrigin): void;
|
|
27
|
+
all(): RegisteredCommand[];
|
|
28
|
+
has(name: string): boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The registry of the running CLI, for internal features that need to list
|
|
32
|
+
* commands (the console's help and completion). Deliberately not part of the
|
|
33
|
+
* public plugin API: exposing it there would freeze the registry's shape.
|
|
34
|
+
*/
|
|
35
|
+
export declare function setActiveRegistry(registry: CommandRegistry): void;
|
|
36
|
+
export declare function activeRegistry(): CommandRegistry | undefined;
|
|
37
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { logger } from "../ui/logger.js";
|
|
2
|
+
function describeOrigin(origin) {
|
|
3
|
+
switch (origin.kind) {
|
|
4
|
+
case "core":
|
|
5
|
+
return "Rowork core";
|
|
6
|
+
case "plugin":
|
|
7
|
+
return `plugin ${origin.pluginName}`;
|
|
8
|
+
case "local":
|
|
9
|
+
return `local file ${origin.file}`;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Registry of every command, whatever its source.
|
|
14
|
+
*
|
|
15
|
+
* Conflict arbitration: core always wins. Without this rule a third-party
|
|
16
|
+
* plugin could silently hijack `rowork init`. Between plugins, first
|
|
17
|
+
* registration wins and the conflict is reported.
|
|
18
|
+
*/
|
|
19
|
+
export class CommandRegistry {
|
|
20
|
+
commands = new Map();
|
|
21
|
+
aliases = new Map();
|
|
22
|
+
register(definition, origin) {
|
|
23
|
+
const existing = this.commands.get(definition.name);
|
|
24
|
+
if (existing !== undefined) {
|
|
25
|
+
if (existing.origin.kind === "core" || origin.kind !== "core") {
|
|
26
|
+
logger.warn(`Skipping command \`${definition.name}\`: already provided by ${describeOrigin(existing.origin)} ` +
|
|
27
|
+
`(attempted by ${describeOrigin(origin)}).`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
logger.warn(`Command \`${definition.name}\` from ${describeOrigin(existing.origin)} replaced by Rowork core.`);
|
|
31
|
+
}
|
|
32
|
+
this.commands.set(definition.name, { definition, origin });
|
|
33
|
+
for (const alias of definition.aliases ?? []) {
|
|
34
|
+
const owner = this.aliases.get(alias);
|
|
35
|
+
if (owner !== undefined && owner !== definition.name) {
|
|
36
|
+
logger.warn(`Skipping alias \`${alias}\`: already taken by \`${owner}\`.`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
this.aliases.set(alias, definition.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
all() {
|
|
43
|
+
return [...this.commands.values()].sort((a, b) => a.definition.name.localeCompare(b.definition.name));
|
|
44
|
+
}
|
|
45
|
+
has(name) {
|
|
46
|
+
return this.commands.has(name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let active;
|
|
50
|
+
/**
|
|
51
|
+
* The registry of the running CLI, for internal features that need to list
|
|
52
|
+
* commands (the console's help and completion). Deliberately not part of the
|
|
53
|
+
* public plugin API: exposing it there would freeze the registry's shape.
|
|
54
|
+
*/
|
|
55
|
+
export function setActiveRegistry(registry) {
|
|
56
|
+
active = registry;
|
|
57
|
+
}
|
|
58
|
+
export function activeRegistry() {
|
|
59
|
+
return active;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModuleDefinition } from "../modules/types.js";
|
|
2
|
+
import { type CommandDefinition } from "../plugins/api.js";
|
|
3
|
+
/** Does running the module with no option ask questions? See `ModuleDefinition.asksQuestions`. */
|
|
4
|
+
export declare function moduleAsks(definition: ModuleDefinition): boolean;
|
|
5
|
+
/** One `add:<name>` command per module, so each can have its own flags. */
|
|
6
|
+
export declare function moduleCommand(definition: ModuleDefinition): CommandDefinition;
|
|
7
|
+
export declare const addCommand: CommandDefinition;
|
|
8
|
+
//# sourceMappingURL=add.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { RoworkError } from "../cli/errors.js";
|
|
2
|
+
import { coreModules } from "../modules/index.js";
|
|
3
|
+
import { installModule, installedModules } from "../core/modules.js";
|
|
4
|
+
import { defineCommand } from "../plugins/api.js";
|
|
5
|
+
import { answered, prompts, requireInteractive } from "../ui/prompt.js";
|
|
6
|
+
import { requireProject } from "./make.js";
|
|
7
|
+
/** Does running the module with no option ask questions? See `ModuleDefinition.asksQuestions`. */
|
|
8
|
+
export function moduleAsks(definition) {
|
|
9
|
+
return definition.asksQuestions ?? (definition.options ?? []).length > 0;
|
|
10
|
+
}
|
|
11
|
+
/** One `add:<name>` command per module, so each can have its own flags. */
|
|
12
|
+
export function moduleCommand(definition) {
|
|
13
|
+
return defineCommand({
|
|
14
|
+
name: `add:${definition.name}`,
|
|
15
|
+
guided: moduleAsks(definition),
|
|
16
|
+
description: `Add the ${definition.title} module: ${definition.description}`,
|
|
17
|
+
options: [
|
|
18
|
+
...(definition.options ?? []),
|
|
19
|
+
...(definition.dependencies === undefined
|
|
20
|
+
? []
|
|
21
|
+
: [{ flags: "--no-install", description: "do not run npm install" }]),
|
|
22
|
+
],
|
|
23
|
+
async run(context) {
|
|
24
|
+
const { root } = requireProject(context, `add:${definition.name}`);
|
|
25
|
+
// Guided when nothing was given on the command line.
|
|
26
|
+
const scripted = Object.keys(context.options).some((key) => key !== "install" && context.options[key] !== undefined);
|
|
27
|
+
// A module with nothing to ask (the linter) runs anywhere, terminal or not.
|
|
28
|
+
const asksQuestions = moduleAsks(definition);
|
|
29
|
+
if (!scripted && asksQuestions) {
|
|
30
|
+
requireInteractive(`add:${definition.name}`, `rowork add:${definition.name} <options> (see --help)`);
|
|
31
|
+
}
|
|
32
|
+
await installModule(context, definition, root, !scripted && asksQuestions);
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export const addCommand = defineCommand({
|
|
37
|
+
name: "add",
|
|
38
|
+
guided: true,
|
|
39
|
+
arguments: [{ name: "module", description: "the feature to add, e.g. player-data (omit it to choose from a list)", required: false }],
|
|
40
|
+
description: "Add a ready-made feature (a working pack of files) to your game, chosen from a list.",
|
|
41
|
+
async run(context) {
|
|
42
|
+
requireProject(context, "add");
|
|
43
|
+
// `rowork add player-data`: the guided version of that module, no list.
|
|
44
|
+
const named = context.args["module"];
|
|
45
|
+
if (typeof named === "string") {
|
|
46
|
+
const wanted = named.replace(/^add:/, "");
|
|
47
|
+
const module = coreModules.find((candidate) => candidate.name === wanted);
|
|
48
|
+
if (module === undefined) {
|
|
49
|
+
const close = coreModules.filter((candidate) => candidate.name.startsWith(wanted.slice(0, 3))).map((candidate) => candidate.name);
|
|
50
|
+
throw new RoworkError(`There is no feature called \`${wanted}\`.`, {
|
|
51
|
+
hint: `Available: ${coreModules.map((candidate) => candidate.name).join(", ")}.${close.length > 0 ? ` Did you mean ${close.join(", ")}?` : ""}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (moduleAsks(module)) {
|
|
55
|
+
requireInteractive(`add ${module.name}`, `rowork add:${module.name} <options> (see --help)`);
|
|
56
|
+
}
|
|
57
|
+
await moduleCommand(module).run({ ...context, args: {}, options: {} });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
requireInteractive("add", "rowork add:<module> (run `rowork --help` to list them)");
|
|
61
|
+
const installed = installedModules(context);
|
|
62
|
+
prompts.intro("rowork add");
|
|
63
|
+
const chosen = answered(await prompts.select({
|
|
64
|
+
message: "Which feature do you want to add? (a working pack, not an empty file)",
|
|
65
|
+
options: coreModules.map((module) => ({
|
|
66
|
+
value: module.name,
|
|
67
|
+
label: installed.includes(module.name) ? `${module.title} (installed)` : module.title,
|
|
68
|
+
hint: module.description,
|
|
69
|
+
})),
|
|
70
|
+
}));
|
|
71
|
+
const module = coreModules.find((candidate) => candidate.name === chosen);
|
|
72
|
+
if (module === undefined)
|
|
73
|
+
return;
|
|
74
|
+
await moduleCommand(module).run({ ...context, args: {}, options: {} });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=add.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Creator } from "../core/open-cloud.js";
|
|
2
|
+
import { type CommandContext } from "../plugins/api.js";
|
|
3
|
+
export declare const KEY_HELP_URL = "https://create.roblox.com/dashboard/credentials";
|
|
4
|
+
export declare function parseCreator(text: string): Creator;
|
|
5
|
+
/** The creator from the flag, the project, or a question. */
|
|
6
|
+
export declare function resolveCreator(context: CommandContext, configured: Creator | undefined): Promise<Creator>;
|
|
7
|
+
/** Remembers the owner in rowork.json. Only the owner: the key never goes there. */
|
|
8
|
+
export declare function saveCreator(root: string, creator: Creator): void;
|
|
9
|
+
/**
|
|
10
|
+
* Walks the user through getting a key and stores it, so nobody has to edit a file by hand.
|
|
11
|
+
* The key is only kept once Roblox has accepted it, and only ever written to `.env`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function setUpApiKey(context: CommandContext, root: string): Promise<{
|
|
14
|
+
key: string;
|
|
15
|
+
source: ".env";
|
|
16
|
+
}>;
|
|
17
|
+
export declare const assetsSetupCommand: import("../plugins/api.js").CommandDefinition;
|
|
18
|
+
//# sourceMappingURL=assets-setup.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { RoworkError } from "../cli/errors.js";
|
|
5
|
+
import { KEY_NAME, looksLikeApiKey, readApiKey, saveApiKey } from "../core/assets.js";
|
|
6
|
+
import { CONFIG_FILENAME } from "../core/config.js";
|
|
7
|
+
import { OpenCloud } from "../core/open-cloud.js";
|
|
8
|
+
import { openBrowser } from "../dashboard/open.js";
|
|
9
|
+
import { defineCommand } from "../plugins/api.js";
|
|
10
|
+
import { answered, isInteractive, prompts } from "../ui/prompt.js";
|
|
11
|
+
import { requireProject } from "./make.js";
|
|
12
|
+
export const KEY_HELP_URL = "https://create.roblox.com/dashboard/credentials";
|
|
13
|
+
export function parseCreator(text) {
|
|
14
|
+
const match = /^(?:(user|group):)?(\d+)$/.exec(text.trim());
|
|
15
|
+
if (match === null) {
|
|
16
|
+
throw new RoworkError(`\`${text}\` is not a creator.`, { hint: "Write user:123456 or group:123456 (a bare number means a user)." });
|
|
17
|
+
}
|
|
18
|
+
return { type: match[1] ?? "user", id: match[2] };
|
|
19
|
+
}
|
|
20
|
+
/** The creator from the flag, the project, or a question. */
|
|
21
|
+
export async function resolveCreator(context, configured) {
|
|
22
|
+
const flag = context.options["creator"];
|
|
23
|
+
if (typeof flag === "string")
|
|
24
|
+
return parseCreator(flag);
|
|
25
|
+
if (configured !== undefined)
|
|
26
|
+
return configured;
|
|
27
|
+
if (!isInteractive()) {
|
|
28
|
+
throw new RoworkError("Roblox needs to know who owns the assets.", {
|
|
29
|
+
hint: "Run once with --creator user:<your user id> (or group:<group id>). It is remembered in rowork.json.",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const type = answered(await prompts.select({
|
|
33
|
+
message: "Who owns the assets you upload?",
|
|
34
|
+
options: [
|
|
35
|
+
{ value: "user", label: "Me (my Roblox account)" },
|
|
36
|
+
{ value: "group", label: "A group" },
|
|
37
|
+
],
|
|
38
|
+
}));
|
|
39
|
+
const id = answered(await prompts.text({
|
|
40
|
+
message: type === "user" ? "Your Roblox user id (the number in your profile address)" : "The group id (the number in the group address)",
|
|
41
|
+
validate: (value) => (/^\d+$/.test((value ?? "").trim()) ? undefined : "Digits only."),
|
|
42
|
+
})).trim();
|
|
43
|
+
return { type, id };
|
|
44
|
+
}
|
|
45
|
+
/** Remembers the owner in rowork.json. Only the owner: the key never goes there. */
|
|
46
|
+
export function saveCreator(root, creator) {
|
|
47
|
+
const file = join(root, CONFIG_FILENAME);
|
|
48
|
+
const config = JSON.parse(readFileSync(file, "utf8"));
|
|
49
|
+
config.assets = { ...config.assets, creator };
|
|
50
|
+
writeFileSync(file, `${JSON.stringify(config, undefined, 2)}\n`, "utf8");
|
|
51
|
+
}
|
|
52
|
+
const STEPS = [
|
|
53
|
+
`1. Open ${KEY_HELP_URL}, click "Create API Key" and give it a name (for example Rowork).`,
|
|
54
|
+
'2. Under "Access Permissions", open "Select API System" and pick "assets".',
|
|
55
|
+
"3. Tick asset:read and asset:write. Nothing else: do not add legacy-assets or asset-permissions,",
|
|
56
|
+
" a key with fewer rights does less damage if it ever leaks.",
|
|
57
|
+
'4. Under "Security", restrict it to your IP address and set an expiry date.',
|
|
58
|
+
'5. Click "Save & Generate Key", then copy the key: Roblox shows it only once.',
|
|
59
|
+
"",
|
|
60
|
+
"Then paste it below. You will also be asked for your Roblox user id (the number in your profile address).",
|
|
61
|
+
].join("\n");
|
|
62
|
+
/**
|
|
63
|
+
* Walks the user through getting a key and stores it, so nobody has to edit a file by hand.
|
|
64
|
+
* The key is only kept once Roblox has accepted it, and only ever written to `.env`.
|
|
65
|
+
*/
|
|
66
|
+
export async function setUpApiKey(context, root) {
|
|
67
|
+
prompts.note(STEPS, "Create a Roblox API key");
|
|
68
|
+
const open = answered(await prompts.confirm({ message: "Open that page in your browser?", initialValue: true }));
|
|
69
|
+
if (open)
|
|
70
|
+
openBrowser(KEY_HELP_URL);
|
|
71
|
+
for (;;) {
|
|
72
|
+
const pasted = answered(await prompts.password({
|
|
73
|
+
message: "Paste the key here (it is hidden, and never shown again)",
|
|
74
|
+
validate: (value) => (looksLikeApiKey((value ?? "").trim()) ? undefined : "That does not look like an API key: paste it whole, without spaces or quotes."),
|
|
75
|
+
})).trim();
|
|
76
|
+
const spinner = prompts.spinner();
|
|
77
|
+
spinner.start("Asking Roblox whether it accepts this key");
|
|
78
|
+
const verdict = await new OpenCloud(pasted).verifyKey();
|
|
79
|
+
if (verdict === "accepted") {
|
|
80
|
+
spinner.stop("Roblox accepts the key.");
|
|
81
|
+
saveApiKey(root, pasted);
|
|
82
|
+
context.logger.success(`Saved in ${pc.bold(".env")} (git ignores that file). Rowork never prints it.`);
|
|
83
|
+
return { key: pasted, source: ".env" };
|
|
84
|
+
}
|
|
85
|
+
spinner.stop(pc.red(verdict === "refused" ? "Roblox refused the key." : "Could not reach Roblox."));
|
|
86
|
+
if (verdict === "refused") {
|
|
87
|
+
context.logger.info("Check that you ticked asset:read and asset:write, that the key has not expired, and that its IP restriction includes this computer.");
|
|
88
|
+
}
|
|
89
|
+
const again = answered(await prompts.confirm({ message: "Try another key?", initialValue: true }));
|
|
90
|
+
if (!again)
|
|
91
|
+
throw new RoworkError("No key was saved.", { hint: `Run \`rowork assets:setup\` again when you have one, or set ${KEY_NAME} yourself.` });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const assetsSetupCommand = defineCommand({
|
|
95
|
+
name: "assets:setup",
|
|
96
|
+
description: "Set up the Roblox API key and owner that `rowork assets` needs (guided, done once).",
|
|
97
|
+
guided: true,
|
|
98
|
+
options: [
|
|
99
|
+
{ flags: "--creator <who>", description: "who owns the uploads: user:<id> or group:<id> (remembered in rowork.json)" },
|
|
100
|
+
{ flags: "--check", description: "only check the key and owner already set up (works without a terminal)" },
|
|
101
|
+
],
|
|
102
|
+
async run(context) {
|
|
103
|
+
const { root, config } = requireProject(context, "assets:setup");
|
|
104
|
+
const { logger } = context;
|
|
105
|
+
const check = context.options["check"] === true;
|
|
106
|
+
if (!check && !isInteractive()) {
|
|
107
|
+
throw new RoworkError("`rowork assets:setup` needs a terminal to ask for the key (it is never taken from an argument).", {
|
|
108
|
+
hint: `Outside a terminal, set ${KEY_NAME} in the environment and run \`rowork assets:setup --check --creator user:<id>\`.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
let found = readApiKey(root);
|
|
112
|
+
if (!check) {
|
|
113
|
+
const replace = found === undefined ||
|
|
114
|
+
answered(await prompts.confirm({ message: `A key is already set up (${found.source}). Replace it?`, initialValue: false }));
|
|
115
|
+
if (replace)
|
|
116
|
+
found = await setUpApiKey(context, root);
|
|
117
|
+
}
|
|
118
|
+
if (found === undefined) {
|
|
119
|
+
throw new RoworkError("No Roblox API key found.", { hint: `Run \`rowork assets:setup\` in a terminal, or set ${KEY_NAME}.` });
|
|
120
|
+
}
|
|
121
|
+
if (check) {
|
|
122
|
+
const verdict = await new OpenCloud(found.key).verifyKey();
|
|
123
|
+
if (verdict !== "accepted") {
|
|
124
|
+
throw new RoworkError(verdict === "refused" ? "Roblox refused the key." : "Could not reach Roblox to check the key.", {
|
|
125
|
+
hint: "Check its permissions (asset:read, asset:write), its expiry and its IP restriction.",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
logger.success(`The key (${found.source}) is accepted by Roblox.`);
|
|
129
|
+
}
|
|
130
|
+
const creator = await resolveCreator(context, config.assets?.creator);
|
|
131
|
+
saveCreator(root, creator);
|
|
132
|
+
logger.success(`Assets will be owned by ${creator.type === "user" ? "user" : "group"} ${creator.id}.`);
|
|
133
|
+
logger.info(`Ready: drop files in ${pc.bold("assets/")} and run \`rowork assets\`.`);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
//# sourceMappingURL=assets-setup.js.map
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { RoworkError } from "../cli/errors.js";
|
|
5
|
+
import { DEFAULT_FOLDER, KEY_NAME, LOCK_FILE, MAX_MODEL_BYTES, MAX_VIDEO_BYTES, assetsFolder, humanSize, planAssets, readApiKey, readLock, renderAssetsModule, scanAssets, writeLock, } from "../core/assets.js";
|
|
6
|
+
import { CONFIG_FILENAME } from "../core/config.js";
|
|
7
|
+
import { OpenCloud } from "../core/open-cloud.js";
|
|
8
|
+
import { defineCommand } from "../plugins/api.js";
|
|
9
|
+
import { answered, isInteractive, prompts } from "../ui/prompt.js";
|
|
10
|
+
import { requireProject } from "./make.js";
|
|
11
|
+
import { KEY_HELP_URL, resolveCreator, saveCreator, setUpApiKey } from "./assets-setup.js";
|
|
12
|
+
const LABEL = { upload: "upload ", resume: "resume ", unchanged: "unchanged" };
|
|
13
|
+
export const assetsCommand = defineCommand({
|
|
14
|
+
name: "assets",
|
|
15
|
+
description: "Upload the files of your assets folder to Roblox, and use their ids by name in your code.",
|
|
16
|
+
guided: true,
|
|
17
|
+
options: [
|
|
18
|
+
{ flags: "--creator <who>", description: "who owns the uploads: user:<id> or group:<id> (remembered in rowork.json)" },
|
|
19
|
+
{ flags: "--dry-run", description: "show what would be uploaded and change nothing" },
|
|
20
|
+
{ flags: "--yes", description: "do not ask for confirmation" },
|
|
21
|
+
],
|
|
22
|
+
async run(context) {
|
|
23
|
+
const { root, config } = requireProject(context, "assets");
|
|
24
|
+
const { logger } = context;
|
|
25
|
+
const folder = assetsFolder(config);
|
|
26
|
+
const folderPath = join(root, folder);
|
|
27
|
+
const { files, skipped } = scanAssets(root, folder);
|
|
28
|
+
if (files.length === 0 && Object.keys(readLock(root).assets).length === 0) {
|
|
29
|
+
mkdirSync(folderPath, { recursive: true });
|
|
30
|
+
writeFileSync(join(folderPath, ".gitkeep"), "", { flag: "a" });
|
|
31
|
+
logger.info(`Nothing to upload yet. Drop images (png, jpg), sounds (mp3, ogg, wav, flac), models (fbx, glb, rbxm) or videos (mp4, mov) in ${pc.bold(`${folder}/`)}, then run \`rowork assets\` again.`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const file of skipped)
|
|
35
|
+
logger.warn(`${file.relative}: ${file.reason}, skipped.`);
|
|
36
|
+
const lock = readLock(root);
|
|
37
|
+
const plan = planAssets(files, lock);
|
|
38
|
+
const todo = plan.filter((entry) => entry.action !== "unchanged");
|
|
39
|
+
logger.info(pc.bold(`${folder}/`));
|
|
40
|
+
for (const entry of plan) {
|
|
41
|
+
logger.step(`${LABEL[entry.action]} ${entry.file.relative} ${pc.dim(`(${entry.file.type}, ${humanSize(entry.file.size)})`)}`);
|
|
42
|
+
}
|
|
43
|
+
logger.blank();
|
|
44
|
+
const present = new Set(files.map((file) => file.relative));
|
|
45
|
+
const writeModule = () => {
|
|
46
|
+
const rendered = renderAssetsModule(readLock(root), present);
|
|
47
|
+
const target = join(root, config.paths.shared);
|
|
48
|
+
mkdirSync(target, { recursive: true });
|
|
49
|
+
writeFileSync(join(target, "assets.ts"), rendered.source, "utf8");
|
|
50
|
+
return { waiting: rendered.waiting };
|
|
51
|
+
};
|
|
52
|
+
if (todo.length === 0) {
|
|
53
|
+
writeModule();
|
|
54
|
+
logger.success("Everything is already uploaded. Your code uses it as `Assets` from shared/assets.");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (context.options["dryRun"] === true) {
|
|
58
|
+
logger.info(`Dry run: ${todo.length} file(s) would be uploaded, nothing was changed.`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// The key and the owner are needed before any question about confirming: fail early.
|
|
62
|
+
let found = readApiKey(root);
|
|
63
|
+
if (found === undefined) {
|
|
64
|
+
if (!isInteractive()) {
|
|
65
|
+
throw new RoworkError("No Roblox API key found.", { hint: `Run \`rowork assets:setup\` in a terminal, or set ${KEY_NAME} in the environment. ${KEY_HELP_URL}` });
|
|
66
|
+
}
|
|
67
|
+
logger.info("No Roblox API key yet: let's set it up, it takes a minute and is done once.");
|
|
68
|
+
found = await setUpApiKey(context, root);
|
|
69
|
+
}
|
|
70
|
+
const creator = await resolveCreator(context, config.assets?.creator);
|
|
71
|
+
if (context.options["yes"] !== true) {
|
|
72
|
+
if (!isInteractive()) {
|
|
73
|
+
throw new RoworkError("`rowork assets` asks for confirmation before uploading.", { hint: "Outside a terminal, add --yes (and --dry-run first to preview)." });
|
|
74
|
+
}
|
|
75
|
+
const owner = `${creator.type === "user" ? "your account" : "group"} ${creator.id}`;
|
|
76
|
+
const confirmed = answered(await prompts.confirm({ message: `Upload ${todo.length} file(s) to Roblox, owned by ${owner}?`, initialValue: true }));
|
|
77
|
+
if (!confirmed) {
|
|
78
|
+
prompts.cancel("Nothing was uploaded.");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
saveCreator(root, creator);
|
|
83
|
+
const cloud = new OpenCloud(found.key);
|
|
84
|
+
let failures = 0;
|
|
85
|
+
for (const { file, action } of todo) {
|
|
86
|
+
try {
|
|
87
|
+
if (action === "upload" && file.type === "Model" && file.size > MAX_MODEL_BYTES) {
|
|
88
|
+
throw new RoworkError(`${file.relative} is ${humanSize(file.size)}: Roblox accepts models up to 20 MB.`);
|
|
89
|
+
}
|
|
90
|
+
if (action === "upload" && file.type === "Video" && file.size > MAX_VIDEO_BYTES) {
|
|
91
|
+
throw new RoworkError(`${file.relative} is ${humanSize(file.size)}: Roblox accepts videos up to 3.75 GB.`);
|
|
92
|
+
}
|
|
93
|
+
let operation = lock.assets[file.relative]?.operation;
|
|
94
|
+
if (action === "upload") {
|
|
95
|
+
operation = await cloud.createAsset({
|
|
96
|
+
file: readFileSync(file.absolute),
|
|
97
|
+
fileName: file.relative.split("/").pop() ?? file.relative,
|
|
98
|
+
contentType: file.contentType,
|
|
99
|
+
assetType: file.type,
|
|
100
|
+
displayName: (file.relative.split("/").pop() ?? file.relative).replace(/\.[^.]+$/, "").slice(0, 50),
|
|
101
|
+
description: "Uploaded with Rowork",
|
|
102
|
+
creator,
|
|
103
|
+
});
|
|
104
|
+
// Written at once: if the run is interrupted, the next one resumes instead of uploading twice.
|
|
105
|
+
lock.assets[file.relative] = { sha256: file.sha256, type: file.type, operation };
|
|
106
|
+
writeLock(root, lock);
|
|
107
|
+
}
|
|
108
|
+
const result = await cloud.waitForOperation(operation);
|
|
109
|
+
if (result.failure !== undefined)
|
|
110
|
+
throw new RoworkError(result.failure);
|
|
111
|
+
const entry = { ...(lock.assets[file.relative] ?? { sha256: file.sha256, type: file.type }) };
|
|
112
|
+
if (result.pending) {
|
|
113
|
+
lock.assets[file.relative] = entry;
|
|
114
|
+
writeLock(root, lock);
|
|
115
|
+
logger.warn(`${file.relative}: Roblox is still processing it. Run \`rowork assets\` again in a moment.`);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
delete entry.operation;
|
|
119
|
+
lock.assets[file.relative] = {
|
|
120
|
+
...entry,
|
|
121
|
+
...(result.assetId === undefined ? {} : { assetId: result.assetId }),
|
|
122
|
+
...(result.moderation === undefined ? {} : { moderation: result.moderation }),
|
|
123
|
+
uploadedAt: new Date().toISOString(),
|
|
124
|
+
};
|
|
125
|
+
writeLock(root, lock);
|
|
126
|
+
logger.success(`${file.relative} -> rbxassetid://${result.assetId ?? "?"}${result.moderation === undefined ? "" : pc.dim(` (${result.moderation})`)}`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const message = error instanceof Error ? cloud.redact(error.message) : "unknown error";
|
|
130
|
+
// A refused key fails every file the same way: stop instead of trying them all.
|
|
131
|
+
if (error instanceof RoworkError && /refused the API key/.test(error.message))
|
|
132
|
+
throw error;
|
|
133
|
+
failures += 1;
|
|
134
|
+
logger.error(`${file.relative}: ${message}`);
|
|
135
|
+
if (error instanceof RoworkError && error.hint !== undefined)
|
|
136
|
+
logger.info(` ${cloud.redact(error.hint)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const { waiting } = writeModule();
|
|
140
|
+
logger.blank();
|
|
141
|
+
logger.info(`Written ${join(config.paths.shared, "assets.ts")}. In your code: ${pc.bold("Assets.<folder>.<name>")}, e.g. \`image.Image = Assets.icons.sword\`.`);
|
|
142
|
+
if (waiting.length > 0) {
|
|
143
|
+
logger.warn("Not in Assets yet (waiting for Roblox, or refused):");
|
|
144
|
+
for (const line of waiting)
|
|
145
|
+
logger.info(` ${line}`);
|
|
146
|
+
logger.info("Run `rowork assets` again later to pick them up.");
|
|
147
|
+
}
|
|
148
|
+
logger.info(pc.dim(`Commit ${LOCK_FILE}: it is what stops files from being uploaded twice. Never commit your API key.`));
|
|
149
|
+
if (failures > 0)
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { RoworkError } from "../cli/errors.js";
|
|
5
|
+
import { activeRegistry } from "../cli/registry.js";
|
|
6
|
+
import { defineCommand } from "../plugins/api.js";
|
|
7
|
+
import { requireInteractive } from "../ui/prompt.js";
|
|
8
|
+
/** Marks child processes so the console cannot be nested inside itself. */
|
|
9
|
+
const IN_CONSOLE = "ROWORK_IN_CONSOLE";
|
|
10
|
+
const BUILTINS = [
|
|
11
|
+
{ name: "help", description: "list the commands" },
|
|
12
|
+
{ name: "clear", description: "clear the screen" },
|
|
13
|
+
{ name: "exit", description: "leave the console (or press Ctrl+D)" },
|
|
14
|
+
];
|
|
15
|
+
/** Splits a line into arguments, keeping "quoted text" together. */
|
|
16
|
+
export function tokenize(line) {
|
|
17
|
+
const tokens = [];
|
|
18
|
+
const pattern = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
19
|
+
for (const match of line.matchAll(pattern)) {
|
|
20
|
+
tokens.push(match[1] ?? match[2] ?? match[3] ?? "");
|
|
21
|
+
}
|
|
22
|
+
return tokens;
|
|
23
|
+
}
|
|
24
|
+
function commandNames() {
|
|
25
|
+
const registered = activeRegistry()?.all().map((entry) => entry.definition.name) ?? [];
|
|
26
|
+
return [...registered, ...BUILTINS.map((builtin) => builtin.name)].sort();
|
|
27
|
+
}
|
|
28
|
+
function printHelp() {
|
|
29
|
+
const entries = [
|
|
30
|
+
...(activeRegistry()?.all().map((entry) => ({
|
|
31
|
+
name: entry.definition.name,
|
|
32
|
+
description: entry.definition.description,
|
|
33
|
+
})) ?? []),
|
|
34
|
+
...BUILTINS,
|
|
35
|
+
].filter((entry) => entry.name !== "console");
|
|
36
|
+
const width = Math.max(...entries.map((entry) => entry.name.length));
|
|
37
|
+
process.stderr.write("\n");
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
process.stderr.write(` ${pc.cyan(entry.name.padEnd(width))} ${entry.description}\n`);
|
|
40
|
+
}
|
|
41
|
+
process.stderr.write(`\n ${pc.dim("Add --help to any command for its options.")}\n\n`);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Runs one line as a normal `rowork` process.
|
|
45
|
+
*
|
|
46
|
+
* A child process rather than an in-process call: guided prompts, `--help`,
|
|
47
|
+
* error reporting and the Ctrl+C handling of `rowork dev` then behave exactly
|
|
48
|
+
* as they do outside the console, with no shared state to leak between
|
|
49
|
+
* commands. The console itself ignores Ctrl+C while a command runs, so
|
|
50
|
+
* stopping `dev` returns to the prompt instead of closing everything.
|
|
51
|
+
*/
|
|
52
|
+
function runLine(tokens, cwd) {
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const script = process.argv[1];
|
|
55
|
+
if (script === undefined) {
|
|
56
|
+
resolve();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const ignore = () => { };
|
|
60
|
+
process.on("SIGINT", ignore);
|
|
61
|
+
const child = spawn(process.execPath, [script, "--cwd", cwd, ...tokens], {
|
|
62
|
+
stdio: "inherit",
|
|
63
|
+
env: { ...process.env, [IN_CONSOLE]: "1" },
|
|
64
|
+
});
|
|
65
|
+
const finish = () => {
|
|
66
|
+
process.off("SIGINT", ignore);
|
|
67
|
+
resolve();
|
|
68
|
+
};
|
|
69
|
+
child.on("error", (error) => {
|
|
70
|
+
process.stderr.write(`${pc.red("error")} ${error.message}\n`);
|
|
71
|
+
finish();
|
|
72
|
+
});
|
|
73
|
+
child.on("close", finish);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export const consoleCommand = defineCommand({
|
|
77
|
+
name: "console",
|
|
78
|
+
guided: true,
|
|
79
|
+
description: "Open an interactive Rowork prompt: type commands without retyping `rowork`.",
|
|
80
|
+
async run(context) {
|
|
81
|
+
if (process.env[IN_CONSOLE] !== undefined) {
|
|
82
|
+
throw new RoworkError("You are already in the Rowork console.");
|
|
83
|
+
}
|
|
84
|
+
requireInteractive("console", "rowork <command>");
|
|
85
|
+
const label = context.config?.name ?? "rowork";
|
|
86
|
+
const promptText = `${pc.bold(pc.cyan(label))}${pc.dim(" >")} `;
|
|
87
|
+
process.stderr.write(`${pc.bold("Rowork console")} ${pc.dim(`v${context.roworkVersion}`)}\n` +
|
|
88
|
+
pc.dim("Type a command (make, dev, studio...). `help` lists them, `exit` leaves.\n\n"));
|
|
89
|
+
const history = [];
|
|
90
|
+
for (;;) {
|
|
91
|
+
// A fresh interface per line: guided commands take over the terminal
|
|
92
|
+
// with their own prompts, and two readers on stdin would fight.
|
|
93
|
+
const rl = createInterface({
|
|
94
|
+
input: process.stdin,
|
|
95
|
+
output: process.stderr,
|
|
96
|
+
history: [...history],
|
|
97
|
+
completer: (line) => {
|
|
98
|
+
const names = commandNames();
|
|
99
|
+
const hits = line.includes(" ") ? [] : names.filter((name) => name.startsWith(line));
|
|
100
|
+
return [hits.length > 0 ? hits : [], line];
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
let line;
|
|
104
|
+
try {
|
|
105
|
+
line = await rl.question(promptText);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Ctrl+D or a closed input.
|
|
109
|
+
rl.close();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
rl.close();
|
|
113
|
+
const tokens = tokenize(line.trim());
|
|
114
|
+
if (tokens[0] === "rowork")
|
|
115
|
+
tokens.shift();
|
|
116
|
+
if (tokens.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
history.unshift(line.trim());
|
|
119
|
+
const [first] = tokens;
|
|
120
|
+
if (first === "exit" || first === "quit")
|
|
121
|
+
return;
|
|
122
|
+
if (first === "clear") {
|
|
123
|
+
process.stderr.write("\x1bc");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (first === "help" || first === "?") {
|
|
127
|
+
printHelp();
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Commander would dump the whole usage for a typo: say it in one line.
|
|
131
|
+
if (first !== undefined && !first.startsWith("-") && !commandNames().includes(first)) {
|
|
132
|
+
const close = commandNames().filter((name) => name.startsWith(first.slice(0, 3)));
|
|
133
|
+
process.stderr.write(`${pc.red("error")} Unknown command \`${first}\`.${close.length > 0 ? ` Did you mean ${close.map((name) => `\`${name}\``).join(", ")}?` : ""} Type \`help\` for the list.\n\n`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
await runLine(tokens, context.cwd);
|
|
137
|
+
process.stderr.write("\n");
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=console.js.map
|