@triflare/fx 1.0.2 → 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.
- package/README.md +32 -0
- package/bin/fx.js +4 -1160
- package/package.json +9 -3
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/build.js +2 -1
- package/src/catalog.js +117 -140
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/index.js +39 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +257 -0
- package/src/cli/define-command.js +140 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +194 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/compiler.js +32 -3
- package/src/config-edit.js +280 -1
- package/src/config.js +137 -3
- package/src/decompiler.js +12 -2
- package/src/extensions.js +311 -48
- package/src/index.js +21 -0
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { outdatedVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { outcome } from "../../define-command.js";
|
|
5
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
export const vendorOutdatedCommand = {
|
|
8
|
+
name: "outdated",
|
|
9
|
+
summary: "Compare upstream sources with fx.lock",
|
|
10
|
+
description: "Check whether any vendored extension changed upstream.",
|
|
11
|
+
examples: ["fx vendor outdated"],
|
|
12
|
+
async run({ cwd, reporter, product }) {
|
|
13
|
+
const configPath = requireConfigPath(cwd, product);
|
|
14
|
+
const configDir = path.dirname(configPath);
|
|
15
|
+
const loaded = loadVendorConfig(configPath);
|
|
16
|
+
if (loaded.vendor.length === 0)
|
|
17
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
18
|
+
|
|
19
|
+
const results = await outdatedVendor({ configDir, config: loaded });
|
|
20
|
+
let outdated = 0;
|
|
21
|
+
let errors = 0;
|
|
22
|
+
for (const result of results) {
|
|
23
|
+
if (result.status === "outdated") outdated += 1;
|
|
24
|
+
if (result.status === "error") errors += 1;
|
|
25
|
+
const label =
|
|
26
|
+
result.status === "outdated"
|
|
27
|
+
? "outdated"
|
|
28
|
+
: result.status === "error"
|
|
29
|
+
? "error"
|
|
30
|
+
: "up to date";
|
|
31
|
+
const tone =
|
|
32
|
+
result.status === "outdated"
|
|
33
|
+
? "warning"
|
|
34
|
+
: result.status === "error"
|
|
35
|
+
? "danger"
|
|
36
|
+
: "success";
|
|
37
|
+
reporter.item(result.name, reporter.tone(tone, label), { width: 24 });
|
|
38
|
+
if (result.error) reporter.warn(result.error);
|
|
39
|
+
}
|
|
40
|
+
if (outdated > 0)
|
|
41
|
+
reporter.warn(
|
|
42
|
+
`${outdated} extension(s) changed upstream; run "${product.command} vendor update"`,
|
|
43
|
+
);
|
|
44
|
+
else if (errors === 0) reporter.success("Everything is up to date");
|
|
45
|
+
|
|
46
|
+
return outcome({
|
|
47
|
+
data: results,
|
|
48
|
+
exitCode: errors > 0 ? 1 : 0,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { removeVendoredExtension } from "../../../vendor.js";
|
|
3
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export const vendorRemoveCommand = {
|
|
6
|
+
name: "remove",
|
|
7
|
+
summary: "Remove a vendored extension",
|
|
8
|
+
description:
|
|
9
|
+
"Remove an extension from the project config, fx.lock, and the vendor directory.",
|
|
10
|
+
arguments: ["<name>"],
|
|
11
|
+
examples: ["fx vendor remove signals"],
|
|
12
|
+
async run({ args, cwd, reporter, prompts, yes, product }) {
|
|
13
|
+
const name = args[0];
|
|
14
|
+
const configPath = requireConfigPath(cwd, product);
|
|
15
|
+
const configDir = path.dirname(configPath);
|
|
16
|
+
const loaded = loadVendorConfig(configPath);
|
|
17
|
+
|
|
18
|
+
await prompts.confirmDestructive({
|
|
19
|
+
message: `Remove vendored extension ${JSON.stringify(name)}?`,
|
|
20
|
+
yes,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = removeVendoredExtension({
|
|
24
|
+
configPath,
|
|
25
|
+
configDir,
|
|
26
|
+
config: loaded,
|
|
27
|
+
name,
|
|
28
|
+
});
|
|
29
|
+
reporter.success(
|
|
30
|
+
`Removed ${name} from ${path.basename(configPath)}, fx.lock and ${result.file}`,
|
|
31
|
+
);
|
|
32
|
+
return { name, file: result.file };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { updateVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
5
|
+
|
|
6
|
+
export const vendorUpdateCommand = {
|
|
7
|
+
name: "update",
|
|
8
|
+
summary: "Update vendored extensions and show diffs",
|
|
9
|
+
description: `Re-fetch upstream sources, update the vendored copies, and show a
|
|
10
|
+
diff for each change. Use --dry-run to preview without writing.`,
|
|
11
|
+
arguments: ["[name]"],
|
|
12
|
+
options: [
|
|
13
|
+
{ flags: "--dry-run", description: "Show diffs without writing files" },
|
|
14
|
+
],
|
|
15
|
+
examples: ["fx vendor update", "fx vendor update signals --dry-run"],
|
|
16
|
+
async run({ args, options, cwd, reporter, product }) {
|
|
17
|
+
const configPath = requireConfigPath(cwd, product);
|
|
18
|
+
const configDir = path.dirname(configPath);
|
|
19
|
+
const loaded = loadVendorConfig(configPath);
|
|
20
|
+
if (loaded.vendor.length === 0)
|
|
21
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
22
|
+
|
|
23
|
+
const result = await updateVendor({
|
|
24
|
+
configPath,
|
|
25
|
+
configDir,
|
|
26
|
+
config: loaded,
|
|
27
|
+
name: args[0],
|
|
28
|
+
write: !options.dryRun,
|
|
29
|
+
});
|
|
30
|
+
for (const entry of result.entries) {
|
|
31
|
+
if (!entry.changed) {
|
|
32
|
+
reporter.info(`${entry.name} is up to date`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
reporter.text(entry.diff);
|
|
36
|
+
}
|
|
37
|
+
if (options.dryRun && result.changed > 0)
|
|
38
|
+
reporter.info(`Dry run: ${result.changed} extension(s) would change`);
|
|
39
|
+
else if (result.changed > 0)
|
|
40
|
+
reporter.success(`Updated ${result.changed} extension(s)`);
|
|
41
|
+
else reporter.success("Everything is up to date");
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
dryRun: Boolean(options.dryRun),
|
|
45
|
+
changed: result.changed,
|
|
46
|
+
entries: result.entries.map(({ name, changed }) => ({ name, changed })),
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { LOCK_FILENAME, verifyVendor } from "../../../vendor.js";
|
|
3
|
+
import { UsageError } from "../../errors.js";
|
|
4
|
+
import { outcome } from "../../define-command.js";
|
|
5
|
+
import { loadVendorConfig, requireConfigPath } from "./helpers.js";
|
|
6
|
+
|
|
7
|
+
export const vendorVerifyCommand = {
|
|
8
|
+
name: "verify",
|
|
9
|
+
summary: "Check vendored files against fx.lock offline",
|
|
10
|
+
description:
|
|
11
|
+
"Verify every vendored extension without making network requests.",
|
|
12
|
+
examples: ["fx vendor verify"],
|
|
13
|
+
run({ cwd, reporter, product }) {
|
|
14
|
+
const configPath = requireConfigPath(cwd, product);
|
|
15
|
+
const configDir = path.dirname(configPath);
|
|
16
|
+
const loaded = loadVendorConfig(configPath);
|
|
17
|
+
if (loaded.vendor.length === 0)
|
|
18
|
+
throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
|
|
19
|
+
|
|
20
|
+
const result = verifyVendor({ configDir, config: loaded });
|
|
21
|
+
for (const warning of result.warnings) reporter.warn(warning);
|
|
22
|
+
for (const entry of result.entries)
|
|
23
|
+
if (entry.status === "ok")
|
|
24
|
+
reporter.item(entry.name, entry.file, { width: 24 });
|
|
25
|
+
|
|
26
|
+
if (!result.ok) {
|
|
27
|
+
for (const problem of result.problems) reporter.error(problem);
|
|
28
|
+
return outcome({
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
data: { entries: result.entries, problems: result.problems },
|
|
31
|
+
error: {
|
|
32
|
+
code: "VENDOR_VERIFY_FAILED",
|
|
33
|
+
message: `Vendor verification failed for ${result.problems.length} extension(s)`,
|
|
34
|
+
details: { problems: result.problems },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
reporter.success(
|
|
40
|
+
`Verified ${result.entries.length} extension(s) against ${LOCK_FILENAME}`,
|
|
41
|
+
);
|
|
42
|
+
return { entries: result.entries };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Generate a bash completion script. */
|
|
2
|
+
export function bashCompletion(command) {
|
|
3
|
+
return `# bash completion for ${command}
|
|
4
|
+
_${command}_complete() {
|
|
5
|
+
local IFS=$'\\n'
|
|
6
|
+
COMPREPLY=( $("${command}" __complete bash -- "\${COMP_WORDS[@]:1}") )
|
|
7
|
+
}
|
|
8
|
+
complete -o bashdefault -o default -F _${command}_complete ${command}
|
|
9
|
+
`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Generate a fish completion script. */
|
|
2
|
+
export function fishCompletion(command) {
|
|
3
|
+
return `# fish completion for ${command}
|
|
4
|
+
function __${command}_complete
|
|
5
|
+
${command} __complete fish -- (commandline -opc)[2..-1]
|
|
6
|
+
end
|
|
7
|
+
complete -c ${command} -f -a '(__${command}_complete)'
|
|
8
|
+
`;
|
|
9
|
+
}
|