@roster-lock/config-editor-cli 0.0.1
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/.claude-plugin/plugin.json +6 -0
- package/LICENSE.md +11 -0
- package/README.md +40 -0
- package/dist/commands/draft/from-lock.js +31 -0
- package/dist/commands/draft/from-lock.js.map +1 -0
- package/dist/commands/draft/index.js +22 -0
- package/dist/commands/draft/index.js.map +1 -0
- package/dist/commands/draft/init.js +25 -0
- package/dist/commands/draft/init.js.map +1 -0
- package/dist/commands/draft/promote.js +24 -0
- package/dist/commands/draft/promote.js.map +1 -0
- package/dist/commands/draft/publish.js +26 -0
- package/dist/commands/draft/publish.js.map +1 -0
- package/dist/commands/draft/validate.js +30 -0
- package/dist/commands/draft/validate.js.map +1 -0
- package/dist/commands/engine/add-asset.js +44 -0
- package/dist/commands/engine/add-asset.js.map +1 -0
- package/dist/commands/engine/add-piece-type.js +47 -0
- package/dist/commands/engine/add-piece-type.js.map +1 -0
- package/dist/commands/engine/edit-definition.js +49 -0
- package/dist/commands/engine/edit-definition.js.map +1 -0
- package/dist/commands/engine/index.js +24 -0
- package/dist/commands/engine/index.js.map +1 -0
- package/dist/commands/engine/remove-asset.js +27 -0
- package/dist/commands/engine/remove-asset.js.map +1 -0
- package/dist/commands/engine/remove-piece-type.js +28 -0
- package/dist/commands/engine/remove-piece-type.js.map +1 -0
- package/dist/commands/engine/set-info.js +22 -0
- package/dist/commands/engine/set-info.js.map +1 -0
- package/dist/commands/engine/show.js +23 -0
- package/dist/commands/engine/show.js.map +1 -0
- package/dist/commands/piece-meta/index.js +19 -0
- package/dist/commands/piece-meta/index.js.map +1 -0
- package/dist/commands/piece-meta/set.js +27 -0
- package/dist/commands/piece-meta/set.js.map +1 -0
- package/dist/commands/piece-meta/show.js +15 -0
- package/dist/commands/piece-meta/show.js.map +1 -0
- package/dist/commands/roster/add-piece.js +93 -0
- package/dist/commands/roster/add-piece.js.map +1 -0
- package/dist/commands/roster/edit-piece.js +92 -0
- package/dist/commands/roster/edit-piece.js.map +1 -0
- package/dist/commands/roster/import-piece.js +83 -0
- package/dist/commands/roster/import-piece.js.map +1 -0
- package/dist/commands/roster/index.js +24 -0
- package/dist/commands/roster/index.js.map +1 -0
- package/dist/commands/roster/list.js +24 -0
- package/dist/commands/roster/list.js.map +1 -0
- package/dist/commands/roster/remove-piece.js +30 -0
- package/dist/commands/roster/remove-piece.js.map +1 -0
- package/dist/commands/roster/rescan.js +55 -0
- package/dist/commands/roster/rescan.js.map +1 -0
- package/dist/commands/roster/test-download.js +72 -0
- package/dist/commands/roster/test-download.js.map +1 -0
- package/dist/commands/selection/add-script.js +43 -0
- package/dist/commands/selection/add-script.js.map +1 -0
- package/dist/commands/selection/index.js +21 -0
- package/dist/commands/selection/index.js.map +1 -0
- package/dist/commands/selection/run-script.js +27 -0
- package/dist/commands/selection/run-script.js.map +1 -0
- package/dist/commands/selection/set.js +41 -0
- package/dist/commands/selection/set.js.map +1 -0
- package/dist/commands/selection/show.js +15 -0
- package/dist/commands/selection/show.js.map +1 -0
- package/dist/commands/show.js +15 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/validate.js +31 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cli-options.js +7 -0
- package/dist/lib/cli-options.js.map +1 -0
- package/dist/lib/draft-io.js +38 -0
- package/dist/lib/draft-io.js.map +1 -0
- package/dist/lib/errors.js +32 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/fs-walk.js +28 -0
- package/dist/lib/fs-walk.js.map +1 -0
- package/dist/lib/json-input.js +16 -0
- package/dist/lib/json-input.js.map +1 -0
- package/dist/lib/lock-io.js +20 -0
- package/dist/lib/lock-io.js.map +1 -0
- package/dist/lib/parse.js +52 -0
- package/dist/lib/parse.js.map +1 -0
- package/dist/lib/piece-scan.js +12 -0
- package/dist/lib/piece-scan.js.map +1 -0
- package/dist/lib/schema-help.js +38 -0
- package/dist/lib/schema-help.js.map +1 -0
- package/dist/lib/schemas.js +71 -0
- package/dist/lib/schemas.js.map +1 -0
- package/dist/lib/semver-report.js +13 -0
- package/dist/lib/semver-report.js.map +1 -0
- package/dist/lib/slugify.js +13 -0
- package/dist/lib/slugify.js.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +49 -0
- package/scripts/bundle.js +15 -0
- package/scripts/generate-version.js +14 -0
- package/skills/rosterlock-config-editor/SKILL.md +151 -0
- package/src/commands/draft/from-lock.ts +34 -0
- package/src/commands/draft/index.ts +5 -0
- package/src/commands/draft/init.ts +21 -0
- package/src/commands/draft/promote.ts +24 -0
- package/src/commands/draft/publish.ts +26 -0
- package/src/commands/draft/validate.ts +30 -0
- package/src/commands/engine/add-asset.ts +46 -0
- package/src/commands/engine/add-piece-type.ts +56 -0
- package/src/commands/engine/edit-definition.ts +58 -0
- package/src/commands/engine/index.ts +7 -0
- package/src/commands/engine/remove-asset.ts +25 -0
- package/src/commands/engine/remove-piece-type.ts +27 -0
- package/src/commands/engine/set-info.ts +17 -0
- package/src/commands/engine/show.ts +20 -0
- package/src/commands/piece-meta/index.ts +2 -0
- package/src/commands/piece-meta/set.ts +29 -0
- package/src/commands/piece-meta/show.ts +12 -0
- package/src/commands/roster/add-piece.ts +109 -0
- package/src/commands/roster/edit-piece.ts +99 -0
- package/src/commands/roster/import-piece.ts +112 -0
- package/src/commands/roster/index.ts +7 -0
- package/src/commands/roster/list.ts +23 -0
- package/src/commands/roster/remove-piece.ts +28 -0
- package/src/commands/roster/rescan.ts +63 -0
- package/src/commands/roster/test-download.ts +76 -0
- package/src/commands/selection/add-script.ts +44 -0
- package/src/commands/selection/index.ts +4 -0
- package/src/commands/selection/run-script.ts +30 -0
- package/src/commands/selection/set.ts +54 -0
- package/src/commands/selection/show.ts +12 -0
- package/src/commands/show.ts +12 -0
- package/src/commands/validate.ts +26 -0
- package/src/index.ts +85 -0
- package/src/lib/cli-options.ts +8 -0
- package/src/lib/draft-io.ts +41 -0
- package/src/lib/errors.ts +30 -0
- package/src/lib/fs-walk.ts +31 -0
- package/src/lib/json-input.ts +21 -0
- package/src/lib/lock-io.ts +18 -0
- package/src/lib/parse.ts +42 -0
- package/src/lib/piece-scan.ts +19 -0
- package/src/lib/schema-help.ts +44 -0
- package/src/lib/schemas.ts +100 -0
- package/src/lib/semver-report.ts +11 -0
- package/src/lib/slugify.ts +9 -0
- package/test.rosterlock.draft.json +62 -0
- package/tests/commands/add-piece.spec.ts +92 -0
- package/tests/commands/draft-validate.spec.ts +85 -0
- package/tests/commands/edit-definition.spec.ts +94 -0
- package/tests/commands/edit-piece.spec.ts +153 -0
- package/tests/commands/from-lock.spec.ts +80 -0
- package/tests/commands/help-arguments.spec.ts +31 -0
- package/tests/commands/import-piece.spec.ts +228 -0
- package/tests/commands/piece-meta.spec.ts +123 -0
- package/tests/commands/rescan.spec.ts +137 -0
- package/tests/lib/draft-io.spec.ts +76 -0
- package/tests/lib/errors.spec.ts +48 -0
- package/tests/lib/fs-walk.spec.ts +35 -0
- package/tests/lib/json-input.spec.ts +63 -0
- package/tests/lib/parse.spec.ts +85 -0
- package/tests/lib/schema-help.spec.ts +42 -0
- package/tests/lib/schemas.spec.ts +82 -0
- package/tests/lib/slugify.spec.ts +28 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +10 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA, validatePieceDefinition } from "@roster-lock/shared";
|
|
3
|
+
import { RosterLockV1Config } from "@roster-lock/types";
|
|
4
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
5
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
6
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
7
|
+
import { parseCommaList, parseStrategy } from "../../lib/parse";
|
|
8
|
+
import { readJsonInput } from "../../lib/json-input";
|
|
9
|
+
import { pieceDefinitionSchema } from "../../lib/schemas";
|
|
10
|
+
import { describeSchemaShape } from "../../lib/schema-help";
|
|
11
|
+
|
|
12
|
+
type PieceDefinition = RosterLockV1Config["engine"]["pieceDefinitions"][string];
|
|
13
|
+
|
|
14
|
+
export const editDefinitionCommand = withDraftOption(new Command("edit-definition"))
|
|
15
|
+
.description(
|
|
16
|
+
"Edit an existing piece type's selection strategy, requires, or path variables " +
|
|
17
|
+
"(use \"engine add-asset\"/\"remove-asset\" to change assets)"
|
|
18
|
+
)
|
|
19
|
+
.argument("<pieceType>", "the piece type key")
|
|
20
|
+
.option("--strategy <strategy>", "mandatory|personal|shared|on-demand")
|
|
21
|
+
.option("--requires <types>", "comma-separated list of required on-demand piece types (replaces the existing list)")
|
|
22
|
+
.option("--path-variables <names>", "comma-separated list of path variable names (replaces the existing list)")
|
|
23
|
+
.option(
|
|
24
|
+
"--json <file>",
|
|
25
|
+
"replace the full piece-type definition (including assets) from a JSON file (or \"-\" for stdin), instead of the flags above. " +
|
|
26
|
+
`Shape: ${describeSchemaShape(pieceDefinitionSchema)}`
|
|
27
|
+
)
|
|
28
|
+
.action(withErrorHandling(async (pieceType: string, opts: {
|
|
29
|
+
draft?: string, strategy?: string, requires?: string, pathVariables?: string, json?: string
|
|
30
|
+
}) => {
|
|
31
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
32
|
+
const draft = readDraft(draftPath);
|
|
33
|
+
|
|
34
|
+
const existing = draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
35
|
+
if(!existing) throw new Error(`Unknown piece type "${pieceType}"; define it first with "engine add-piece-type"`);
|
|
36
|
+
|
|
37
|
+
let definition: PieceDefinition;
|
|
38
|
+
if(opts.json){
|
|
39
|
+
definition = await readJsonInput(opts.json, pieceDefinitionSchema);
|
|
40
|
+
} else {
|
|
41
|
+
if(opts.strategy === undefined && opts.requires === undefined && opts.pathVariables === undefined){
|
|
42
|
+
throw new Error("Nothing to change; pass --strategy, --requires, --path-variables, or --json");
|
|
43
|
+
}
|
|
44
|
+
definition = {
|
|
45
|
+
...existing,
|
|
46
|
+
selectionStrategy: opts.strategy !== undefined ? parseStrategy(opts.strategy) : existing.selectionStrategy,
|
|
47
|
+
requires: opts.requires !== undefined ? parseCommaList(opts.requires) : existing.requires,
|
|
48
|
+
pathVariables: opts.pathVariables !== undefined ? parseCommaList(opts.pathVariables) : existing.pathVariables,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
draft.stagedLock.engine.pieceDefinitions[pieceType] = definition;
|
|
53
|
+
validatePieceDefinition(pieceType, definition, draft.stagedLock.engine);
|
|
54
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
55
|
+
|
|
56
|
+
writeDraft(draftPath, draft);
|
|
57
|
+
console.log(`Updated piece type: ${pieceType}`);
|
|
58
|
+
}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
3
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
4
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
5
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
6
|
+
|
|
7
|
+
export const removeAssetCommand = withDraftOption(new Command("remove-asset"))
|
|
8
|
+
.description("Remove an asset definition from a piece type")
|
|
9
|
+
.argument("<pieceType>", "the piece type key")
|
|
10
|
+
.argument("<assetName>", "the asset's name")
|
|
11
|
+
.action(withErrorHandling(async (pieceType: string, assetName: string, opts: { draft?: string }) => {
|
|
12
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
13
|
+
const draft = readDraft(draftPath);
|
|
14
|
+
|
|
15
|
+
const definition = draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
16
|
+
if(!definition) throw new Error(`Unknown piece type "${pieceType}"`);
|
|
17
|
+
const index = definition.assets.findIndex((a) => a.name === assetName);
|
|
18
|
+
if(index === -1) throw new Error(`Unknown asset "${assetName}" on piece type "${pieceType}"`);
|
|
19
|
+
|
|
20
|
+
definition.assets.splice(index, 1);
|
|
21
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
22
|
+
|
|
23
|
+
writeDraft(draftPath, draft);
|
|
24
|
+
console.log(`Removed asset "${assetName}" from piece type "${pieceType}"`);
|
|
25
|
+
}));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
3
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
4
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
5
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
6
|
+
|
|
7
|
+
export const removePieceTypeCommand = withDraftOption(new Command("remove-piece-type"))
|
|
8
|
+
.description("Remove a piece type definition from the staged lock's engine")
|
|
9
|
+
.argument("<pieceType>", "the piece type key")
|
|
10
|
+
.action(withErrorHandling(async (pieceType: string, opts: { draft?: string }) => {
|
|
11
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
12
|
+
const draft = readDraft(draftPath);
|
|
13
|
+
|
|
14
|
+
if(!draft.stagedLock.engine.pieceDefinitions[pieceType]){
|
|
15
|
+
throw new Error(`Unknown piece type "${pieceType}"`);
|
|
16
|
+
}
|
|
17
|
+
const existingPieces = draft.stagedLock.rosters[pieceType];
|
|
18
|
+
if(existingPieces && existingPieces.length > 0){
|
|
19
|
+
throw new Error(`Piece type "${pieceType}" still has pieces in its roster; remove them first with "roster remove-piece"`);
|
|
20
|
+
}
|
|
21
|
+
delete draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
22
|
+
delete draft.stagedLock.rosters[pieceType];
|
|
23
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
24
|
+
|
|
25
|
+
writeDraft(draftPath, draft);
|
|
26
|
+
console.log(`Removed piece type: ${pieceType}`);
|
|
27
|
+
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
3
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
4
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
5
|
+
|
|
6
|
+
export const setInfoCommand = withDraftOption(new Command("set-info"))
|
|
7
|
+
.description("Set the engine name/version on the staged lock")
|
|
8
|
+
.option("--name <name>", "engine name")
|
|
9
|
+
.option("--engine-version <version>", "engine version")
|
|
10
|
+
.action(withErrorHandling(async (opts: { draft?: string, name?: string, engineVersion?: string }) => {
|
|
11
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
12
|
+
const draft = readDraft(draftPath);
|
|
13
|
+
if(opts.name !== undefined) draft.stagedLock.engine.name = opts.name;
|
|
14
|
+
if(opts.engineVersion !== undefined) draft.stagedLock.engine.version = opts.engineVersion;
|
|
15
|
+
writeDraft(draftPath, draft);
|
|
16
|
+
console.log(`engine: ${draft.stagedLock.engine.name}@${draft.stagedLock.engine.version}`);
|
|
17
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolveDraftPath, readDraft } from "../../lib/draft-io";
|
|
3
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
4
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
5
|
+
|
|
6
|
+
export const engineShowCommand = withDraftOption(new Command("show"))
|
|
7
|
+
.description("Print the staged lock's engine config, or a single piece type")
|
|
8
|
+
.argument("[pieceType]", "the piece type key (defaults to showing the whole engine config)")
|
|
9
|
+
.action(withErrorHandling(async (pieceType: string | undefined, opts: { draft?: string }) => {
|
|
10
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
11
|
+
const draft = readDraft(draftPath);
|
|
12
|
+
|
|
13
|
+
if(pieceType){
|
|
14
|
+
const definition = draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
15
|
+
if(!definition) throw new Error(`Unknown piece type "${pieceType}"`);
|
|
16
|
+
console.log(JSON.stringify(definition, null, 2));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.log(JSON.stringify(draft.stagedLock.engine, null, 2));
|
|
20
|
+
}));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA, EMPTY_PIECE_META } from "@roster-lock/shared";
|
|
3
|
+
import { cloneJSON } from "@roster-lock/utils";
|
|
4
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
5
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
6
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
7
|
+
import { readJsonInput } from "../../lib/json-input";
|
|
8
|
+
import { pieceMetaOverridesSchema } from "../../lib/schemas";
|
|
9
|
+
import { describeSchemaShape } from "../../lib/schema-help";
|
|
10
|
+
|
|
11
|
+
export const pieceMetaSetCommand = withDraftOption(new Command("set"))
|
|
12
|
+
.description("Set the custom piece meta (schema/defaultMeta/values) for a piece type")
|
|
13
|
+
.argument("<pieceType>", "the piece type key")
|
|
14
|
+
.option(
|
|
15
|
+
"--json <file>",
|
|
16
|
+
"fields to merge in, read from a JSON file (or \"-\" for stdin). " +
|
|
17
|
+
`Shape: ${describeSchemaShape(pieceMetaOverridesSchema)}`
|
|
18
|
+
)
|
|
19
|
+
.action(withErrorHandling(async (pieceType: string, opts: { draft?: string, json?: string }) => {
|
|
20
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
21
|
+
const draft = readDraft(draftPath);
|
|
22
|
+
|
|
23
|
+
const overrides = opts.json ? await readJsonInput(opts.json, pieceMetaOverridesSchema) : {};
|
|
24
|
+
draft.stagedLock.pieceMeta[pieceType] = { ...cloneJSON(EMPTY_PIECE_META), ...overrides };
|
|
25
|
+
|
|
26
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
27
|
+
writeDraft(draftPath, draft);
|
|
28
|
+
console.log(`Set piece meta for "${pieceType}"`);
|
|
29
|
+
}));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolveDraftPath, readDraft } from "../../lib/draft-io";
|
|
3
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
4
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
5
|
+
|
|
6
|
+
export const pieceMetaShowCommand = withDraftOption(new Command("show"))
|
|
7
|
+
.description("Print the staged lock's piece meta")
|
|
8
|
+
.action(withErrorHandling(async (opts: { draft?: string }) => {
|
|
9
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
10
|
+
const draft = readDraft(draftPath);
|
|
11
|
+
console.log(JSON.stringify(draft.stagedLock.pieceMeta, null, 2));
|
|
12
|
+
}));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join as pathJoin } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA,
|
|
6
|
+
ROSTERLOCK_V1_PIECEINFO_CASTER_JSONSCHEMA,
|
|
7
|
+
PATH_ROSTERLOCK_PIECE_INFO,
|
|
8
|
+
} from "@roster-lock/shared";
|
|
9
|
+
import { RosterLockPiece } from "@roster-lock/types";
|
|
10
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
11
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
12
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
13
|
+
import { parseKeyValueList } from "../../lib/parse";
|
|
14
|
+
import { scanPieceFolder } from "../../lib/piece-scan";
|
|
15
|
+
import { slugify } from "../../lib/slugify";
|
|
16
|
+
import { readJsonInput } from "../../lib/json-input";
|
|
17
|
+
import { pieceOverridesSchema } from "../../lib/schemas";
|
|
18
|
+
import { describeSchemaShape } from "../../lib/schema-help";
|
|
19
|
+
|
|
20
|
+
function collectSource(value: string, previous: Array<string>){
|
|
21
|
+
previous.push(value);
|
|
22
|
+
return previous;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const addPieceCommand = withDraftOption(new Command("add-piece"))
|
|
26
|
+
.description("Scan a folder and add it as a piece for an existing piece type")
|
|
27
|
+
.argument("<pieceType>", "the piece type key")
|
|
28
|
+
.argument("<folder>", "path to the folder to scan for the piece's assets")
|
|
29
|
+
.option("--path-variables <k=v,...>", "path variable values used to resolve this piece's asset globs")
|
|
30
|
+
.option("--download-source <url>", "download source url (repeatable)", collectSource, [] as Array<string>)
|
|
31
|
+
.option(
|
|
32
|
+
"--json <file>",
|
|
33
|
+
"set humanInfo/downloadSources/pathVariables from a JSON file (or \"-\" for stdin), applied on top of a " +
|
|
34
|
+
`rosterlock.piece-info.json in the folder if present. Shape: ${describeSchemaShape(pieceOverridesSchema)}`
|
|
35
|
+
)
|
|
36
|
+
.action(withErrorHandling(async (pieceType: string, folder: string, opts: {
|
|
37
|
+
draft?: string, pathVariables?: string, downloadSource: Array<string>, json?: string
|
|
38
|
+
}) => {
|
|
39
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
40
|
+
const draft = readDraft(draftPath);
|
|
41
|
+
|
|
42
|
+
const pieceDefinition = draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
43
|
+
if(!pieceDefinition) throw new Error(`Unknown piece type "${pieceType}"; define it first with "engine add-piece-type"`);
|
|
44
|
+
|
|
45
|
+
const pathVariables = parseKeyValueList(opts.pathVariables);
|
|
46
|
+
const { version, errors } = await scanPieceFolder(folder, pathVariables, pieceDefinition);
|
|
47
|
+
if(errors.length > 0){
|
|
48
|
+
for(const err of errors) console.error(`[${err.type}] ${err.id}: ${err.message}`);
|
|
49
|
+
throw new Error(`Folder does not match piece type "${pieceType}"'s asset definitions`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const piece: RosterLockPiece = {
|
|
53
|
+
id: "",
|
|
54
|
+
version,
|
|
55
|
+
humanInfo: { name: "", author: "", url: "" },
|
|
56
|
+
downloadSources: [...opts.downloadSource],
|
|
57
|
+
pathVariables,
|
|
58
|
+
requiredPieces: Object.fromEntries(
|
|
59
|
+
pieceDefinition.requires.map((requiredType) => [requiredType, { selectable: false, expected: [] }])
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const metaPath = pathJoin(folder, PATH_ROSTERLOCK_PIECE_INFO);
|
|
64
|
+
if(existsSync(metaPath)){
|
|
65
|
+
const metaJson = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
66
|
+
const meta = ROSTERLOCK_V1_PIECEINFO_CASTER_JSONSCHEMA.cast(metaJson, true);
|
|
67
|
+
piece.humanInfo = meta.humanInfo;
|
|
68
|
+
for(const source of meta.downloadSources){
|
|
69
|
+
if(!piece.downloadSources.includes(source)) piece.downloadSources.push(source);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if(opts.json){
|
|
74
|
+
const overrides = await readJsonInput(opts.json, pieceOverridesSchema);
|
|
75
|
+
if(overrides.humanInfo) piece.humanInfo = overrides.humanInfo;
|
|
76
|
+
if(overrides.downloadSources){
|
|
77
|
+
for(const source of overrides.downloadSources){
|
|
78
|
+
if(!piece.downloadSources.includes(source)) piece.downloadSources.push(source);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if(overrides.pathVariables) Object.assign(piece.pathVariables, overrides.pathVariables);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if(piece.downloadSources.length === 0){
|
|
85
|
+
throw new Error(
|
|
86
|
+
"At least one download source is required; pass --download-source or provide a rosterlock.piece-info.json with downloadSources"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
piece.id = piece.humanInfo.name
|
|
91
|
+
? `@${slugify(piece.humanInfo.author)}/${slugify(piece.humanInfo.name)}`
|
|
92
|
+
: `#${version.logic}/${version.media}`;
|
|
93
|
+
|
|
94
|
+
const collection = draft.stagedLock.rosters[pieceType] ?? (draft.stagedLock.rosters[pieceType] = []);
|
|
95
|
+
if(collection.find((p) => p.id === piece.id)){
|
|
96
|
+
throw new Error(`Piece "${piece.id}" already exists in "${pieceType}"; use "roster remove-piece" first`);
|
|
97
|
+
}
|
|
98
|
+
collection.push(piece);
|
|
99
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
100
|
+
|
|
101
|
+
draft.draft.rosterPieceInfo[pieceType] ??= {};
|
|
102
|
+
draft.draft.rosterPieceInfo[pieceType][piece.id] = {
|
|
103
|
+
referenceFolder: folder,
|
|
104
|
+
testedDownloadSources: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
writeDraft(draftPath, draft);
|
|
108
|
+
console.log(`Added piece "${piece.id}" (logic=${version.logic}) to "${pieceType}"`);
|
|
109
|
+
}));
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
3
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
4
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
5
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
6
|
+
import { parseKeyValueList } from "../../lib/parse";
|
|
7
|
+
import { readJsonInput } from "../../lib/json-input";
|
|
8
|
+
import { pieceOverridesSchema } from "../../lib/schemas";
|
|
9
|
+
import { describeSchemaShape } from "../../lib/schema-help";
|
|
10
|
+
|
|
11
|
+
function collect(value: string, previous: Array<string>){
|
|
12
|
+
previous.push(value);
|
|
13
|
+
return previous;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const editPieceCommand = withDraftOption(new Command("edit-piece"))
|
|
17
|
+
.description("Edit an existing roster piece's human info, download sources, or path variables")
|
|
18
|
+
.argument("<pieceType>", "the piece type key")
|
|
19
|
+
.argument("<pieceId>", "the piece's id (as shown by \"roster list\")")
|
|
20
|
+
.option("--name <name>", "human-readable name")
|
|
21
|
+
.option("--author <author>", "author name")
|
|
22
|
+
.option("--url <url>", "info url")
|
|
23
|
+
.option("--image <url>", "image url")
|
|
24
|
+
.option("--add-download-source <url>", "download source url to add (repeatable)", collect, [] as Array<string>)
|
|
25
|
+
.option("--remove-download-source <url>", "download source url to remove (repeatable)", collect, [] as Array<string>)
|
|
26
|
+
.option("--path-variables <k=v,...>", "path variable values to set (existing keys not mentioned are left alone)")
|
|
27
|
+
.option("--remove-path-variable <name>", "path variable name to remove (repeatable)", collect, [] as Array<string>)
|
|
28
|
+
.option(
|
|
29
|
+
"--json <file>",
|
|
30
|
+
"bulk-set humanInfo/downloadSources/pathVariables from a JSON file (or \"-\" for stdin), applied before the " +
|
|
31
|
+
`flags above (so a specific flag always wins over --json). Shape: ${describeSchemaShape(pieceOverridesSchema)}`
|
|
32
|
+
)
|
|
33
|
+
.action(withErrorHandling(async (pieceType: string, pieceId: string, opts: {
|
|
34
|
+
draft?: string,
|
|
35
|
+
name?: string, author?: string, url?: string, image?: string,
|
|
36
|
+
addDownloadSource: Array<string>, removeDownloadSource: Array<string>,
|
|
37
|
+
pathVariables?: string, removePathVariable: Array<string>,
|
|
38
|
+
json?: string,
|
|
39
|
+
}) => {
|
|
40
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
41
|
+
const draft = readDraft(draftPath);
|
|
42
|
+
|
|
43
|
+
const collection = draft.stagedLock.rosters[pieceType];
|
|
44
|
+
if(!collection) throw new Error(`Unknown piece type "${pieceType}"`);
|
|
45
|
+
const piece = collection.find((p) => p.id === pieceId);
|
|
46
|
+
if(!piece) throw new Error(`Unknown piece "${pieceId}" in "${pieceType}"`);
|
|
47
|
+
|
|
48
|
+
const nothingRequested = (
|
|
49
|
+
opts.name === undefined && opts.author === undefined && opts.url === undefined && opts.image === undefined
|
|
50
|
+
&& opts.addDownloadSource.length === 0 && opts.removeDownloadSource.length === 0
|
|
51
|
+
&& opts.pathVariables === undefined && opts.removePathVariable.length === 0
|
|
52
|
+
&& opts.json === undefined
|
|
53
|
+
);
|
|
54
|
+
if(nothingRequested){
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Nothing to change; pass --name/--author/--url/--image, " +
|
|
57
|
+
"--add-download-source/--remove-download-source, --path-variables/--remove-path-variable, or --json"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if(opts.json){
|
|
62
|
+
const overrides = await readJsonInput(opts.json, pieceOverridesSchema);
|
|
63
|
+
if(overrides.humanInfo) piece.humanInfo = overrides.humanInfo;
|
|
64
|
+
if(overrides.downloadSources){
|
|
65
|
+
for(const source of overrides.downloadSources){
|
|
66
|
+
if(!piece.downloadSources.includes(source)) piece.downloadSources.push(source);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if(overrides.pathVariables) Object.assign(piece.pathVariables, overrides.pathVariables);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if(opts.name !== undefined) piece.humanInfo.name = opts.name;
|
|
73
|
+
if(opts.author !== undefined) piece.humanInfo.author = opts.author;
|
|
74
|
+
if(opts.url !== undefined) piece.humanInfo.url = opts.url;
|
|
75
|
+
if(opts.image !== undefined) piece.humanInfo.image = opts.image;
|
|
76
|
+
|
|
77
|
+
for(const source of opts.addDownloadSource){
|
|
78
|
+
if(!piece.downloadSources.includes(source)) piece.downloadSources.push(source);
|
|
79
|
+
}
|
|
80
|
+
for(const source of opts.removeDownloadSource){
|
|
81
|
+
const index = piece.downloadSources.indexOf(source);
|
|
82
|
+
if(index === -1) throw new Error(`Download source not found on piece "${pieceId}": ${source}`);
|
|
83
|
+
piece.downloadSources.splice(index, 1);
|
|
84
|
+
}
|
|
85
|
+
if(piece.downloadSources.length === 0){
|
|
86
|
+
throw new Error("A piece must keep at least one download source");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if(opts.pathVariables !== undefined){
|
|
90
|
+
Object.assign(piece.pathVariables, parseKeyValueList(opts.pathVariables));
|
|
91
|
+
}
|
|
92
|
+
for(const name of opts.removePathVariable){
|
|
93
|
+
delete piece.pathVariables[name];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
97
|
+
writeDraft(draftPath, draft);
|
|
98
|
+
console.log(`Updated piece "${pieceId}" in "${pieceType}"`);
|
|
99
|
+
}));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
4
|
+
import { cloneJSON } from "@roster-lock/utils";
|
|
5
|
+
import { RosterLockPiece, RosterLockV1Config } from "@roster-lock/types";
|
|
6
|
+
import { resolveDraftPath, readDraft, writeDraft, DRAFT_SUFFIX } from "../../lib/draft-io";
|
|
7
|
+
import { readLock } from "../../lib/lock-io";
|
|
8
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
9
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
10
|
+
|
|
11
|
+
function readSourceConfig(sourcePath: string): RosterLockV1Config {
|
|
12
|
+
const resolved = resolve(process.cwd(), sourcePath);
|
|
13
|
+
if(resolved.endsWith(DRAFT_SUFFIX)) return readDraft(resolved).stagedLock;
|
|
14
|
+
return readLock(resolved);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findByMatchingVersion(collection: Array<RosterLockPiece> | undefined, version: RosterLockPiece["version"]){
|
|
18
|
+
return collection?.find((p) => p.version.logic === version.logic && p.version.media === version.media);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Pulls in whatever the piece needs so its requiredPieces.expected ids resolve in the target doc: reuse an
|
|
22
|
+
// existing piece of matching logic/media version if one's already there (fixing up the expected id to match it),
|
|
23
|
+
// otherwise copy the source piece over verbatim under its original id.
|
|
24
|
+
function importRequiredPieces(
|
|
25
|
+
sourceConfig: RosterLockV1Config, targetConfig: RosterLockV1Config, piece: RosterLockPiece, importing: Set<string>
|
|
26
|
+
){
|
|
27
|
+
for(const [requiredType, requirement] of Object.entries(piece.requiredPieces)){
|
|
28
|
+
requirement.expected = requirement.expected.map(
|
|
29
|
+
(requiredId) => importRequiredPiece(sourceConfig, targetConfig, requiredType, requiredId, importing)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function importRequiredPiece(
|
|
35
|
+
sourceConfig: RosterLockV1Config, targetConfig: RosterLockV1Config,
|
|
36
|
+
pieceType: string, sourceId: string, importing: Set<string>
|
|
37
|
+
): string {
|
|
38
|
+
const sourcePiece = sourceConfig.rosters[pieceType]?.find((p) => p.id === sourceId);
|
|
39
|
+
if(!sourcePiece) throw new Error(`Required piece "${sourceId}" of type "${pieceType}" not found in the source document`);
|
|
40
|
+
|
|
41
|
+
const collection = targetConfig.rosters[pieceType] ?? (targetConfig.rosters[pieceType] = []);
|
|
42
|
+
const existing = findByMatchingVersion(collection, sourcePiece.version);
|
|
43
|
+
if(existing) return existing.id;
|
|
44
|
+
|
|
45
|
+
const importKey = `${pieceType}:${sourceId}`;
|
|
46
|
+
if(importing.has(importKey)){
|
|
47
|
+
throw new Error(`Circular required-piece reference detected at "${pieceType}" piece "${sourceId}"`);
|
|
48
|
+
}
|
|
49
|
+
if(collection.find((p) => p.id === sourceId)){
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Cannot import required piece "${sourceId}" (${pieceType}): a different piece already exists under that id ` +
|
|
52
|
+
"in the target draft"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const requiredPiece = cloneJSON(sourcePiece);
|
|
57
|
+
collection.push(requiredPiece);
|
|
58
|
+
|
|
59
|
+
importing.add(importKey);
|
|
60
|
+
importRequiredPieces(sourceConfig, targetConfig, requiredPiece, importing);
|
|
61
|
+
importing.delete(importKey);
|
|
62
|
+
|
|
63
|
+
console.log(
|
|
64
|
+
`Imported required piece "${requiredPiece.id}" into "${pieceType}": ` +
|
|
65
|
+
`logic ${requiredPiece.version.logic}, media ${requiredPiece.version.media}`
|
|
66
|
+
);
|
|
67
|
+
return requiredPiece.id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const importPieceCommand = withDraftOption(new Command("import-piece"))
|
|
71
|
+
.description(
|
|
72
|
+
"Copy a piece from another draft or published lock file into this draft's rosters, trusting the source's " +
|
|
73
|
+
"existing hash as-is - use \"roster add-piece\" instead if you need the hash (re)computed from local files"
|
|
74
|
+
)
|
|
75
|
+
.argument("<sourcePath>", `path to the source draft (*${DRAFT_SUFFIX}) or published lock file`)
|
|
76
|
+
.argument("<pieceType>", "the piece type key (must exist in this draft's engine)")
|
|
77
|
+
.argument("<pieceId>", "the piece's id in the source (as shown by \"roster list\" run against the source)")
|
|
78
|
+
.option("--as <newId>", "import under a different piece id (defaults to the source piece's id)")
|
|
79
|
+
.action(withErrorHandling(async (sourcePath: string, pieceType: string, pieceId: string, opts: {
|
|
80
|
+
draft?: string, as?: string
|
|
81
|
+
}) => {
|
|
82
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
83
|
+
const draft = readDraft(draftPath);
|
|
84
|
+
|
|
85
|
+
const pieceDefinition = draft.stagedLock.engine.pieceDefinitions[pieceType];
|
|
86
|
+
if(!pieceDefinition) throw new Error(`Unknown piece type "${pieceType}"; define it first with "engine add-piece-type"`);
|
|
87
|
+
|
|
88
|
+
const sourceConfig = readSourceConfig(sourcePath);
|
|
89
|
+
const sourceCollection = sourceConfig.rosters[pieceType];
|
|
90
|
+
const sourcePiece = sourceCollection?.find((p) => p.id === pieceId);
|
|
91
|
+
if(!sourcePiece) throw new Error(`Unknown piece "${pieceId}" in "${pieceType}" in "${sourcePath}"`);
|
|
92
|
+
|
|
93
|
+
const newId = opts.as ?? sourcePiece.id;
|
|
94
|
+
const collection = draft.stagedLock.rosters[pieceType] ?? (draft.stagedLock.rosters[pieceType] = []);
|
|
95
|
+
if(collection.find((p) => p.id === newId)){
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Piece "${newId}" already exists in "${pieceType}"; use --as to import under a different id, ` +
|
|
98
|
+
`or "roster remove-piece" first`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const piece: RosterLockPiece = { ...cloneJSON(sourcePiece), id: newId };
|
|
103
|
+
collection.push(piece);
|
|
104
|
+
importRequiredPieces(sourceConfig, draft.stagedLock, piece, new Set([`${pieceType}:${sourcePiece.id}`]));
|
|
105
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
106
|
+
|
|
107
|
+
writeDraft(draftPath, draft);
|
|
108
|
+
console.log(
|
|
109
|
+
`Imported piece "${newId}"${newId !== sourcePiece.id ? ` (from "${sourcePiece.id}")` : ""} into "${pieceType}": ` +
|
|
110
|
+
`logic ${piece.version.logic}, media ${piece.version.media}`
|
|
111
|
+
);
|
|
112
|
+
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolveDraftPath, readDraft } from "../../lib/draft-io";
|
|
3
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
4
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
5
|
+
|
|
6
|
+
export const rosterListCommand = withDraftOption(new Command("list"))
|
|
7
|
+
.description("List pieces in the staged lock's rosters")
|
|
8
|
+
.argument("[pieceType]", "the piece type key (defaults to listing every piece type's roster)")
|
|
9
|
+
.action(withErrorHandling(async (pieceType: string | undefined, opts: { draft?: string }) => {
|
|
10
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
11
|
+
const draft = readDraft(draftPath);
|
|
12
|
+
|
|
13
|
+
const collections = pieceType
|
|
14
|
+
? { [pieceType]: draft.stagedLock.rosters[pieceType] ?? [] }
|
|
15
|
+
: draft.stagedLock.rosters;
|
|
16
|
+
|
|
17
|
+
for(const [type, pieces] of Object.entries(collections)){
|
|
18
|
+
console.log(`${type}:`);
|
|
19
|
+
for(const piece of pieces){
|
|
20
|
+
console.log(` ${piece.id} logic=${piece.version.logic.slice(0, 12)} sources=${piece.downloadSources.length}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ROSTERLOCK_V1_CASTER_JSONSCHEMA } from "@roster-lock/shared";
|
|
3
|
+
import { resolveDraftPath, readDraft, writeDraft } from "../../lib/draft-io";
|
|
4
|
+
import { withDraftOption } from "../../lib/cli-options";
|
|
5
|
+
import { withErrorHandling } from "../../lib/errors";
|
|
6
|
+
|
|
7
|
+
export const removePieceCommand = withDraftOption(new Command("remove-piece"))
|
|
8
|
+
.description("Remove a piece from the staged lock's rosters")
|
|
9
|
+
.argument("<pieceType>", "the piece type key")
|
|
10
|
+
.argument("<pieceId>", "the piece's id (as shown by \"roster list\")")
|
|
11
|
+
.action(withErrorHandling(async (pieceType: string, pieceId: string, opts: { draft?: string }) => {
|
|
12
|
+
const draftPath = resolveDraftPath(opts.draft);
|
|
13
|
+
const draft = readDraft(draftPath);
|
|
14
|
+
|
|
15
|
+
const collection = draft.stagedLock.rosters[pieceType];
|
|
16
|
+
if(!collection) throw new Error(`Unknown piece type "${pieceType}"`);
|
|
17
|
+
const index = collection.findIndex((p) => p.id === pieceId);
|
|
18
|
+
if(index === -1) throw new Error(`Unknown piece "${pieceId}" in "${pieceType}"`);
|
|
19
|
+
|
|
20
|
+
collection.splice(index, 1);
|
|
21
|
+
ROSTERLOCK_V1_CASTER_JSONSCHEMA.cast(draft.stagedLock);
|
|
22
|
+
|
|
23
|
+
const pieceInfo = draft.draft.rosterPieceInfo[pieceType];
|
|
24
|
+
if(pieceInfo) delete pieceInfo[pieceId];
|
|
25
|
+
|
|
26
|
+
writeDraft(draftPath, draft);
|
|
27
|
+
console.log(`Removed piece "${pieceId}" from "${pieceType}"`);
|
|
28
|
+
}));
|