@triflare/fx 1.0.3 → 2.0.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.
Files changed (50) hide show
  1. package/README.md +32 -0
  2. package/bin/fx.js +4 -1162
  3. package/package.json +5 -3
  4. package/product.yml +1 -1
  5. package/src/brand.js +9 -3
  6. package/src/cli/commands/build.js +98 -0
  7. package/src/cli/commands/catalog.js +42 -0
  8. package/src/cli/commands/check.js +44 -0
  9. package/src/cli/commands/completion.js +54 -0
  10. package/src/cli/commands/decompile.js +266 -0
  11. package/src/cli/commands/dev.js +179 -0
  12. package/src/cli/commands/extension.js +110 -0
  13. package/src/cli/commands/index.js +39 -0
  14. package/src/cli/commands/init.js +169 -0
  15. package/src/cli/commands/package.js +137 -0
  16. package/src/cli/commands/sprite/add.js +85 -0
  17. package/src/cli/commands/sprite/build.js +62 -0
  18. package/src/cli/commands/sprite/helpers.js +66 -0
  19. package/src/cli/commands/sprite/index.js +29 -0
  20. package/src/cli/commands/sprite/list.js +31 -0
  21. package/src/cli/commands/sprite/remove.js +51 -0
  22. package/src/cli/commands/vendor/add.js +98 -0
  23. package/src/cli/commands/vendor/helpers.js +50 -0
  24. package/src/cli/commands/vendor/index.js +39 -0
  25. package/src/cli/commands/vendor/install.js +64 -0
  26. package/src/cli/commands/vendor/list.js +45 -0
  27. package/src/cli/commands/vendor/migrate.js +47 -0
  28. package/src/cli/commands/vendor/outdated.js +51 -0
  29. package/src/cli/commands/vendor/remove.js +34 -0
  30. package/src/cli/commands/vendor/update.js +49 -0
  31. package/src/cli/commands/vendor/verify.js +44 -0
  32. package/src/cli/completion/bash.js +10 -0
  33. package/src/cli/completion/fish.js +9 -0
  34. package/src/cli/completion/index.js +97 -0
  35. package/src/cli/completion/zsh.js +11 -0
  36. package/src/cli/context.js +257 -0
  37. package/src/cli/define-command.js +140 -0
  38. package/src/cli/errors.js +111 -0
  39. package/src/cli/format.js +31 -0
  40. package/src/cli/prompts.js +183 -0
  41. package/src/cli/reporter.js +194 -0
  42. package/src/cli/run.js +248 -0
  43. package/src/cli/templates/index.js +112 -0
  44. package/src/cli/theme.js +84 -0
  45. package/src/config-edit.js +280 -1
  46. package/src/config.js +137 -3
  47. package/src/extensions.js +72 -19
  48. package/src/index.js +21 -0
  49. package/src/vendor.js +1323 -0
  50. package/vendor/pm-packager-dist/.fx-build.json +1 -1
@@ -0,0 +1,85 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { findConfigPath } from "../../../config.js";
4
+ import { sourceExtension } from "../../../brand.js";
5
+ import { slugify } from "../../../package.js";
6
+ import { parse } from "../../../parser.js";
7
+ import { readSprite, spriteSource } from "../../../sprite.js";
8
+ import { CliError } from "../../errors.js";
9
+ import {
10
+ readInputBytes,
11
+ spriteNames,
12
+ spriteRoot,
13
+ spriteSourceDir,
14
+ } from "./helpers.js";
15
+
16
+ export const spriteAddCommand = {
17
+ name: "add",
18
+ summary: "Import a .sprite3/.pms sprite as source",
19
+ description: `Import a sprite archive, write its assets, and generate a .tfx
20
+ source file.`,
21
+ arguments: ["<source>"],
22
+ options: [
23
+ { flags: "--name <name>", description: "Override the sprite name" },
24
+ { flags: "--force", description: "Overwrite an existing source file" },
25
+ ],
26
+ examples: [
27
+ "fx sprite add robot.sprite3",
28
+ "fx sprite add https://example.com/cat.pms --name Cat",
29
+ ],
30
+ async run({ args, options, cwd, reporter, product }) {
31
+ const source = args[0];
32
+ const configPath = findConfigPath(cwd);
33
+ const rootDir = spriteRoot(cwd, configPath);
34
+
35
+ const { sprite, assets } = readSprite(await readInputBytes(source, cwd));
36
+ const name = typeof options.name === "string" ? options.name : sprite.name;
37
+ if (!name)
38
+ throw new CliError("sprite add could not read a sprite name", {
39
+ code: "SPRITE_NAME_MISSING",
40
+ hint: "pass --name <name>",
41
+ });
42
+ sprite.name = name;
43
+
44
+ const existing = spriteNames(rootDir).find((entry) => entry.name === name);
45
+ if (existing)
46
+ throw new CliError(
47
+ `A sprite named ${JSON.stringify(name)} already exists in ${existing.file}`,
48
+ {
49
+ code: "SPRITE_EXISTS",
50
+ hint: "remove it first, or choose another name with --name",
51
+ },
52
+ );
53
+
54
+ const sourceDir = spriteSourceDir(rootDir, product);
55
+ const file = path.join(
56
+ sourceDir,
57
+ `${slugify(name)}${sourceExtension(product)}`,
58
+ );
59
+ if (fs.existsSync(file) && !options.force)
60
+ throw new CliError(`${file} already exists`, {
61
+ code: "SPRITE_FILE_EXISTS",
62
+ hint: "pass --force to overwrite",
63
+ });
64
+
65
+ const assetsDir = path.join(rootDir, "assets");
66
+ const relative = path.relative(sourceDir, assetsDir).replace(/\\/g, "/");
67
+ const text = spriteSource(sprite, {
68
+ assetPrefix: relative ? `${relative}/` : "",
69
+ });
70
+ parse(text, file);
71
+
72
+ fs.mkdirSync(assetsDir, { recursive: true });
73
+ for (const [assetName, data] of assets)
74
+ fs.writeFileSync(path.join(assetsDir, assetName), data);
75
+ fs.mkdirSync(sourceDir, { recursive: true });
76
+ fs.writeFileSync(file, text);
77
+
78
+ reporter.success(`Added sprite ${name} -> ${path.relative(rootDir, file)}`);
79
+ return {
80
+ name,
81
+ file,
82
+ assets: [...assets.keys()],
83
+ };
84
+ },
85
+ };
@@ -0,0 +1,62 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { build } from "../../../build.js";
4
+ import { outputDirectory } from "../../../brand.js";
5
+ import { slugify } from "../../../package.js";
6
+ import { spriteArchive } from "../../../sprite.js";
7
+ import { loadProjectState } from "../../context.js";
8
+ import { CliError } from "../../errors.js";
9
+
10
+ export const spriteBuildCommand = {
11
+ name: "build",
12
+ summary: "Compile one sprite into a .pms archive",
13
+ description:
14
+ "Build a single sprite from the current project into a .pms file.",
15
+ arguments: ["<name>"],
16
+ options: [{ flags: "-o, --output <file>", description: "Output .pms path" }],
17
+ examples: ["fx sprite build Cat", "fx sprite build Cat -o cat.pms"],
18
+ async run({ args, options, cwd, reporter, prompts }) {
19
+ const name = args[0];
20
+ const state = await loadProjectState({
21
+ input: undefined,
22
+ cwd,
23
+ options,
24
+ reporter,
25
+ prompts,
26
+ });
27
+ const result = build({
28
+ ...state.target,
29
+ projectConfig: state.loaded.project,
30
+ fxConfig: state.loaded.fx,
31
+ catalog: state.catalog,
32
+ extensionUrls: state.extensionUrls,
33
+ useConfigFile: false,
34
+ });
35
+
36
+ const sprite = result.project.targets.find(
37
+ (target) => !target.isStage && target.name === name,
38
+ );
39
+ if (!sprite) {
40
+ const available = result.project.targets
41
+ .filter((target) => !target.isStage)
42
+ .map((target) => target.name);
43
+ throw new CliError(
44
+ `No sprite named ${JSON.stringify(name)}. ` +
45
+ (available.length
46
+ ? `Available: ${available.join(", ")}`
47
+ : "The project has no sprites."),
48
+ { code: "SPRITE_NOT_FOUND" },
49
+ );
50
+ }
51
+
52
+ const output =
53
+ options.output !== undefined
54
+ ? path.resolve(cwd, options.output)
55
+ : path.join(cwd, outputDirectory(), `${slugify(name)}.pms`);
56
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
57
+ fs.writeFileSync(output, spriteArchive(sprite, result.assets));
58
+
59
+ reporter.success(`Built sprite ${name} -> ${output}`);
60
+ return { name, output };
61
+ },
62
+ };
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse } from "../../../parser.js";
4
+ import { discoverSources } from "../../../project.js";
5
+ import { sourceDirectory } from "../../../brand.js";
6
+
7
+ /** Read a local file or download a remote sprite archive. */
8
+ export async function readInputBytes(source, cwd = process.cwd()) {
9
+ if (/^https?:\/\//i.test(source)) {
10
+ const response = await fetch(source, {
11
+ signal: AbortSignal.timeout(15000),
12
+ });
13
+ if (!response.ok)
14
+ throw new Error(
15
+ `Could not download ${source}: ${response.status} ${response.statusText}`,
16
+ );
17
+ return Buffer.from(await response.arrayBuffer());
18
+ }
19
+ return fs.readFileSync(path.resolve(cwd, source));
20
+ }
21
+
22
+ /** Every sprite declaration under `dir`, in source order. */
23
+ export function spriteNames(dir) {
24
+ const sprites = [];
25
+ for (const file of discoverSources(dir)) {
26
+ const ast = parse(fs.readFileSync(file, "utf8"), file);
27
+ for (const sprite of ast.sprites) {
28
+ sprites.push({ name: sprite.name, file, partial: sprite.partial });
29
+ }
30
+ }
31
+ return sprites;
32
+ }
33
+
34
+ /** Group sprite declarations by name, tracking the base declaration. */
35
+ export function spriteGroups(dir) {
36
+ const groups = new Map();
37
+ for (const entry of spriteNames(dir)) {
38
+ let group = groups.get(entry.name);
39
+ if (!group) {
40
+ group = {
41
+ name: entry.name,
42
+ file: entry.file,
43
+ files: [],
44
+ partial: true,
45
+ };
46
+ groups.set(entry.name, group);
47
+ }
48
+ group.files.push(entry.file);
49
+ if (!entry.partial) {
50
+ group.file = entry.file;
51
+ group.partial = false;
52
+ }
53
+ }
54
+ for (const group of groups.values())
55
+ group.files = [...new Set([group.file, ...group.files])];
56
+ return [...groups.values()];
57
+ }
58
+
59
+ /** Root directory that owns sources and assets. */
60
+ export function spriteRoot(cwd, configPath) {
61
+ return configPath ? path.dirname(configPath) : cwd;
62
+ }
63
+
64
+ export function spriteSourceDir(rootDir, product) {
65
+ return path.join(rootDir, sourceDirectory(product));
66
+ }
@@ -0,0 +1,29 @@
1
+ import { Command } from "commander";
2
+ import { defineCommand } from "../../define-command.js";
3
+ import { spriteAddCommand } from "./add.js";
4
+ import { spriteBuildCommand } from "./build.js";
5
+ import { spriteListCommand } from "./list.js";
6
+ import { spriteRemoveCommand } from "./remove.js";
7
+
8
+ const SUBCOMMANDS = [
9
+ spriteAddCommand,
10
+ spriteBuildCommand,
11
+ spriteRemoveCommand,
12
+ spriteListCommand,
13
+ ];
14
+
15
+ export function registerSprite(program, options) {
16
+ const sprite = new Command("sprite")
17
+ .description("Import, build, list, and remove sprites.")
18
+ .helpGroup("Extension Commands:");
19
+ sprite.copyInheritedSettings(program);
20
+ sprite.addHelpText(
21
+ "after",
22
+ `\nExamples:\n fx sprite add robot.sprite3\n fx sprite build Cat\n fx sprite list --json\n`,
23
+ );
24
+ sprite.action(() => sprite.outputHelp());
25
+ program.addCommand(sprite);
26
+
27
+ for (const spec of SUBCOMMANDS) defineCommand(sprite, spec, options);
28
+ return sprite;
29
+ }
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+ import { spriteGroups } from "./helpers.js";
3
+
4
+ export const spriteListCommand = {
5
+ name: "list",
6
+ summary: "List declared sprites",
7
+ description: "List every sprite declared under a directory.",
8
+ arguments: ["[dir]"],
9
+ examples: ["fx sprite list", "fx sprite list src --json"],
10
+ run({ args, cwd, reporter }) {
11
+ const dir = path.resolve(cwd, args[0] || ".");
12
+ const sprites = spriteGroups(dir);
13
+ if (sprites.length === 0) {
14
+ reporter.info(`No sprites under ${dir}`);
15
+ } else {
16
+ for (const { name, files } of sprites)
17
+ reporter.item(
18
+ name,
19
+ files.map((file) => path.relative(dir, file) || file).join(", "),
20
+ { width: 24 },
21
+ );
22
+ }
23
+
24
+ return sprites.map(({ name, file, files, partial }) => ({
25
+ name,
26
+ file: path.relative(dir, file),
27
+ files: files.map((entry) => path.relative(dir, entry)),
28
+ partial,
29
+ }));
30
+ },
31
+ };
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parse } from "../../../parser.js";
4
+ import { CliError } from "../../errors.js";
5
+ import { spriteNames } from "./helpers.js";
6
+
7
+ export const spriteRemoveCommand = {
8
+ name: "remove",
9
+ summary: "Delete files that declare only one sprite",
10
+ description: `Delete the source file(s) that declare a sprite. Files that also
11
+ contain other code are refused.`,
12
+ arguments: ["<name>"],
13
+ examples: ["fx sprite remove Cat"],
14
+ async run({ args, cwd, reporter, prompts, yes }) {
15
+ const name = args[0];
16
+ const matches = spriteNames(cwd).filter((entry) => entry.name === name);
17
+ if (matches.length === 0)
18
+ throw new CliError(
19
+ `No sprite named ${JSON.stringify(name)} under ${cwd}`,
20
+ { code: "SPRITE_NOT_FOUND" },
21
+ );
22
+
23
+ const files = [...new Set(matches.map((entry) => entry.file))];
24
+ for (const file of files) {
25
+ const ast = parse(fs.readFileSync(file, "utf8"), file);
26
+ const onlyThisSprite =
27
+ ast.sprites.length > 0 &&
28
+ ast.sprites.every((sprite) => sprite.name === name) &&
29
+ ast.topItems.length === 0 &&
30
+ !ast.stage;
31
+ if (!onlyThisSprite)
32
+ throw new CliError(
33
+ `${file} also declares other code; remove the sprite block by hand`,
34
+ { code: "SPRITE_SHARED_FILE" },
35
+ );
36
+ }
37
+
38
+ await prompts.confirmDestructive({
39
+ message: `Delete ${files.length} file(s) for sprite ${JSON.stringify(name)}?`,
40
+ yes,
41
+ });
42
+
43
+ for (const file of files) fs.rmSync(file);
44
+ reporter.success(
45
+ `Removed sprite ${name} (${files
46
+ .map((file) => path.relative(cwd, file) || file)
47
+ .join(", ")})`,
48
+ );
49
+ return { name, files };
50
+ },
51
+ };
@@ -0,0 +1,98 @@
1
+ import path from "node:path";
2
+ import { defaultVendorName, installVendorEntry } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import {
5
+ loadVendorConfig,
6
+ requireConfigPath,
7
+ vendorConfigSource,
8
+ } from "./helpers.js";
9
+
10
+ /** Shared implementation for `vendor add` and legacy `vendor install <src>`. */
11
+ export async function addVendor({
12
+ source,
13
+ options,
14
+ cwd,
15
+ reporter,
16
+ product,
17
+ }) {
18
+ if (!source) throw new UsageError("vendor add requires a path or URL");
19
+ const configPath = requireConfigPath(cwd, product);
20
+ const configDir = path.dirname(configPath);
21
+ const loaded = loadVendorConfig(configPath);
22
+
23
+ if (typeof options.as === "string" && options.as.trim() === "")
24
+ throw new UsageError("--as needs a non-empty name");
25
+ const name =
26
+ typeof options.as === "string" ? options.as : defaultVendorName(source);
27
+ const configSource = vendorConfigSource(source, configDir, cwd);
28
+ const existing = loaded.vendor.find((entry) => entry.name === name);
29
+ if (existing && existing.source !== configSource && !options.force)
30
+ throw new UsageError(
31
+ `A vendored extension named ${JSON.stringify(name)} already exists.`,
32
+ {
33
+ hint: `run "${product.command} vendor update ${name}" or pass --force to replace it`,
34
+ },
35
+ );
36
+
37
+ const rawEntry = loaded.raw?.vendor?.[name];
38
+ const existingVolatile =
39
+ rawEntry &&
40
+ typeof rawEntry === "object" &&
41
+ Object.hasOwn(rawEntry, "volatile") &&
42
+ rawEntry.volatile != null
43
+ ? Boolean(rawEntry.volatile)
44
+ : undefined;
45
+ const result = await installVendorEntry({
46
+ configPath,
47
+ configDir,
48
+ name,
49
+ source: configSource,
50
+ vendorDir: path.resolve(configDir, loaded.vendorDir || "extensions"),
51
+ unsandboxed: Boolean(options.unsandboxed) || Boolean(existing?.unsandboxed),
52
+ volatile: existingVolatile,
53
+ });
54
+
55
+ reporter.success(
56
+ `Vendored ${name} -> ${result.file} (${result.lockEntry.sha256.slice(0, 12)}…)`,
57
+ );
58
+ if (existing)
59
+ reporter.info(`Updated ${name} in ${path.basename(configPath)}`);
60
+ else reporter.success(`Added ${name} to ${path.basename(configPath)}`);
61
+
62
+ return {
63
+ name,
64
+ source: configSource,
65
+ file: result.file,
66
+ sha256: result.lockEntry.sha256,
67
+ replaced: Boolean(existing),
68
+ };
69
+ }
70
+
71
+ export const vendorAddCommand = {
72
+ name: "add",
73
+ summary: "Vendor an extension by URL or path",
74
+ description: `Copy an extension into the vendor directory, record its SHA-256 in
75
+ fx.lock, and add it to the project config.`,
76
+ arguments: ["<source>"],
77
+ options: [
78
+ { flags: "--as <name>", description: "Name for the vendored extension" },
79
+ {
80
+ flags: "--unsandboxed",
81
+ description: "Allow the extension to run unsandboxed",
82
+ },
83
+ { flags: "--force", description: "Replace an existing entry with the name" },
84
+ ],
85
+ examples: [
86
+ "fx vendor add https://example.com/signals.js --as signals",
87
+ "fx vendor add ./src/mything.js",
88
+ ],
89
+ run(context) {
90
+ return addVendor({
91
+ source: context.args[0],
92
+ options: context.options,
93
+ cwd: context.cwd,
94
+ reporter: context.reporter,
95
+ product: context.product,
96
+ });
97
+ },
98
+ };
@@ -0,0 +1,50 @@
1
+ import path from "node:path";
2
+ import { configFilename } from "../../../brand.js";
3
+ import { findConfigPath, loadConfigFrom } from "../../../config.js";
4
+ import { resolveVendorFiles } from "../../../vendor.js";
5
+ import { UsageError } from "../../errors.js";
6
+
7
+ /** Find the project config or explain how to create one. */
8
+ export function requireConfigPath(cwd, product) {
9
+ const found = findConfigPath(cwd);
10
+ if (!found)
11
+ throw new UsageError(`No ${configFilename(product)} found.`, {
12
+ hint: `run "${product?.command ?? "fx"} init" first`,
13
+ });
14
+ return found;
15
+ }
16
+
17
+ export function loadVendorConfig(configPath) {
18
+ return loadConfigFrom(path.dirname(configPath));
19
+ }
20
+
21
+ /** Turn a command-line local path into a path relative to the config file. */
22
+ export function vendorConfigSource(source, configDir, cwd = process.cwd()) {
23
+ if (/^https?:\/\//i.test(source) || /^data:/i.test(source)) return source;
24
+ const absolute = path.isAbsolute(source)
25
+ ? source
26
+ : path.resolve(cwd, source);
27
+ const relative = path.relative(configDir, absolute).split(path.sep).join("/");
28
+ if (!relative) return ".";
29
+ return relative.startsWith(".") ? relative : `./${relative}`;
30
+ }
31
+
32
+ export function vendorEntriesWithLock(loaded, lock, configDir) {
33
+ const dirName = loaded.vendorDir || "extensions";
34
+ const dir = path.resolve(configDir, dirName);
35
+ const files = resolveVendorFiles(configDir, dir, loaded.vendor || [], lock);
36
+ return (loaded.vendor || []).map((entry) => {
37
+ const { file } = files.get(entry.name);
38
+ return {
39
+ name: entry.name,
40
+ source: entry.source,
41
+ file,
42
+ remote: entry.remote,
43
+ local: entry.local,
44
+ volatile: entry.volatile,
45
+ unsandboxed: entry.unsandboxed,
46
+ sha256: lock.extensions[entry.name]?.sha256 ?? null,
47
+ fetched: lock.extensions[entry.name]?.fetched ?? null,
48
+ };
49
+ });
50
+ }
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import { defineCommand } from "../../define-command.js";
3
+ import { vendorAddCommand } from "./add.js";
4
+ import { vendorInstallCommand } from "./install.js";
5
+ import { vendorListCommand } from "./list.js";
6
+ import { vendorMigrateCommand } from "./migrate.js";
7
+ import { vendorOutdatedCommand } from "./outdated.js";
8
+ import { vendorRemoveCommand } from "./remove.js";
9
+ import { vendorUpdateCommand } from "./update.js";
10
+ import { vendorVerifyCommand } from "./verify.js";
11
+
12
+ const SUBCOMMANDS = [
13
+ vendorAddCommand,
14
+ vendorInstallCommand,
15
+ vendorUpdateCommand,
16
+ vendorOutdatedCommand,
17
+ vendorVerifyCommand,
18
+ vendorRemoveCommand,
19
+ vendorListCommand,
20
+ vendorMigrateCommand,
21
+ ];
22
+
23
+ export function registerVendor(program, options) {
24
+ const vendor = new Command("vendor")
25
+ .description(
26
+ "Copy extensions into the vendor directory and load them from there.",
27
+ )
28
+ .helpGroup("Extension Commands:");
29
+ vendor.copyInheritedSettings(program);
30
+ vendor.addHelpText(
31
+ "after",
32
+ `\nExamples:\n fx vendor add https://example.com/ext.js --as ext\n fx vendor install\n fx vendor update --dry-run\n`,
33
+ );
34
+ vendor.action(() => vendor.outputHelp());
35
+ program.addCommand(vendor);
36
+
37
+ for (const spec of SUBCOMMANDS) defineCommand(vendor, spec, options);
38
+ return vendor;
39
+ }
@@ -0,0 +1,64 @@
1
+ import path from "node:path";
2
+ import { LOCK_FILENAME, installVendor } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { addVendor } from "./add.js";
5
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
6
+
7
+ export const vendorInstallCommand = {
8
+ name: "install",
9
+ summary: "Restore vendored extensions from fx.lock",
10
+ description: `Fetch only the vendored files that are missing, verifying them
11
+ against fx.lock. Without a source, restores every configured entry.`,
12
+ arguments: ["[source]"],
13
+ options: [
14
+ { flags: "--as <name>", description: "Name for the vendored extension" },
15
+ {
16
+ flags: "--unsandboxed",
17
+ description: "Allow the extension to run unsandboxed",
18
+ },
19
+ { flags: "--force", description: "Replace an existing entry with the name" },
20
+ ],
21
+ examples: ["fx vendor install", "fx vendor add https://example.com/ext.js"],
22
+ async run(context) {
23
+ const { args, options, cwd, reporter, product } = context;
24
+ const source = args[0];
25
+ if (source) {
26
+ reporter.warn(
27
+ '"vendor install <source>" is deprecated; use "vendor add <source>".',
28
+ );
29
+ return addVendor({ source, options, cwd, reporter, product });
30
+ }
31
+
32
+ const configPath = requireConfigPath(cwd, product);
33
+ const configDir = path.dirname(configPath);
34
+ const loaded = loadVendorConfig(configPath);
35
+ if (loaded.vendor.length === 0)
36
+ throw new UsageError(
37
+ `No vendor entries in ${path.basename(configPath)}.`,
38
+ {
39
+ hint: `add one with "${product.command} vendor add <url|path>"`,
40
+ },
41
+ );
42
+
43
+ const result = await installVendor({
44
+ configPath,
45
+ configDir,
46
+ config: loaded,
47
+ });
48
+ for (const warning of result.warnings) reporter.warn(warning);
49
+ for (const entry of result.entries)
50
+ reporter.success(`${entry.name}: ${entry.status} (${entry.file})`);
51
+ reporter.success(
52
+ `Installed ${result.entries.length} extension(s) from ${LOCK_FILENAME}`,
53
+ );
54
+
55
+ return {
56
+ lock: LOCK_FILENAME,
57
+ entries: result.entries.map(({ name, status, file }) => ({
58
+ name,
59
+ status,
60
+ file,
61
+ })),
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { readLock } from "../../../vendor.js";
3
+ import {
4
+ loadVendorConfig,
5
+ requireConfigPath,
6
+ vendorEntriesWithLock,
7
+ } from "./helpers.js";
8
+
9
+ export const vendorListCommand = {
10
+ name: "list",
11
+ summary: "List vendored extensions",
12
+ description: "List every extension declared in the vendor config.",
13
+ examples: ["fx vendor list", "fx vendor list --json"],
14
+ run({ cwd, reporter, product }) {
15
+ const configPath = requireConfigPath(cwd, product);
16
+ const configDir = path.dirname(configPath);
17
+ const loaded = loadVendorConfig(configPath);
18
+ const lock = readLock(configDir);
19
+ const entries = vendorEntriesWithLock(loaded, lock, configDir);
20
+
21
+ reporter.heading(
22
+ `${path.basename(configPath)}: ${entries.length} vendored extension(s) in ${
23
+ loaded.vendorDir || "extensions"
24
+ }/`,
25
+ );
26
+ for (const entry of entries) {
27
+ const flags = [
28
+ entry.remote ? "remote" : entry.local ? "local" : "embedded",
29
+ entry.volatile ? "volatile" : null,
30
+ entry.unsandboxed ? "unsandboxed" : null,
31
+ ]
32
+ .filter(Boolean)
33
+ .join(", ");
34
+ reporter.item(entry.name, entry.file, {
35
+ width: 24,
36
+ suffix: flags ? `[${flags}]` : "",
37
+ });
38
+ }
39
+
40
+ return {
41
+ vendorDir: loaded.vendorDir || "extensions",
42
+ extensions: entries,
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import { migrateProjectExtensions } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
5
+
6
+ export const vendorMigrateCommand = {
7
+ name: "migrate",
8
+ summary: "Move project.extensions under vendor:",
9
+ description: `Move legacy project.extensions entries into the vendor system,
10
+ writing a .bak copy of the config first.`,
11
+ examples: ["fx vendor migrate", "fx vendor migrate --yes"],
12
+ async run({ cwd, reporter, prompts, yes, product }) {
13
+ const configPath = requireConfigPath(cwd, product);
14
+ const configDir = path.dirname(configPath);
15
+ const loaded = loadVendorConfig(configPath);
16
+ if (loaded.project.extensions.length === 0)
17
+ throw new UsageError(
18
+ `No project.extensions to migrate in ${path.basename(configPath)}`,
19
+ );
20
+
21
+ const interactive = prompts.enabled && !yes;
22
+ const result = await migrateProjectExtensions({
23
+ configPath,
24
+ configDir,
25
+ config: loaded,
26
+ nameFor: interactive
27
+ ? (name, source) =>
28
+ prompts.text({
29
+ message: `Name for ${source}`,
30
+ defaultValue: name,
31
+ })
32
+ : undefined,
33
+ });
34
+ for (const warning of result.warnings) reporter.warn(warning);
35
+ if (result.diff) reporter.text(result.diff);
36
+ reporter.success(`Backed up ${path.basename(configPath)} -> ${result.backup}`);
37
+ reporter.success(`Migrated ${result.migrated.length} extension(s) to vendor:`);
38
+ for (const entry of result.migrated)
39
+ reporter.item(entry.name, entry.file, { width: 24 });
40
+
41
+ return {
42
+ backup: result.backup,
43
+ migrated: result.migrated.map(({ name, file }) => ({ name, file })),
44
+ warnings: result.warnings,
45
+ };
46
+ },
47
+ };