@ssobig/writer-cli 0.3.0 → 0.3.3
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 +11 -4
- package/asset-repository.js +7 -2
- package/package.json +1 -1
- package/templates/mystery-v1/authoring-view-preference.js +25 -2
- package/templates/mystery-v1/codemirror6-runtime.min.js +1 -1
- package/templates/mystery-v1/component-asset-operations.js +19 -6
- package/templates/mystery-v1/component-catalog-contract.js +2 -2
- package/templates/mystery-v1/component-field-contracts.js +98 -2
- package/templates/mystery-v1/component-navigation-counts.js +6 -1
- package/templates/mystery-v1/component-renderers.js +1 -1
- package/templates/mystery-v1/component-storage-contract.js +28 -9
- package/templates/mystery-v1/markdown-document-model.js +458 -0
- package/templates/mystery-v1/markdown-image-editor.js +336 -0
- package/templates/mystery-v1/markdown-live-editor.js +1246 -110
- package/templates/mystery-v1/markdown-toolbar.js +514 -0
- package/templates/mystery-v1/timeline-model.js +235 -0
- package/tools/writer-cli/package-lock.json +2 -2
- package/tools/writer-cli/package.json +1 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +12 -13
- package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
- package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
- package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -1
- package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
- package/tools/writer-cli/src/command-registry.cjs +24 -23
- package/tools/writer-cli/src/commands.cjs +22 -1
- package/tools/writer-cli/src/domain.cjs +46 -0
- package/tools/writer-cli/src/project-import.cjs +15 -1
|
@@ -41,44 +41,45 @@ const COMMANDS = Object.freeze([
|
|
|
41
41
|
command("version", "Show installed CLI version and integrity.", [option("check", null, "Check npm latest.", { type: "boolean" })], ["ssobig-writer version --check"]),
|
|
42
42
|
command("doctor", "Run read-only installation and connectivity diagnostics.", [], ["ssobig-writer doctor"]),
|
|
43
43
|
command("command-schema", "Discover groups, leaves, or exact machine-readable command metadata.", [positional("scope", "Optional group or group.command scope.", { required: false })], ["ssobig-writer command-schema component.get --compact"]),
|
|
44
|
-
command("auth.login", "Sign in through the
|
|
45
|
-
command("auth.whoami", "Show the current authenticated
|
|
44
|
+
command("auth.login", "Sign in through the Writer member Google sign-in flow.", [option("no-open", null, "Print the login URL without opening a browser.", { type: "boolean" })], ["ssobig-writer auth login --no-open"], SERVER),
|
|
45
|
+
command("auth.whoami", "Show the current authenticated Writer member identity.", [], ["ssobig-writer auth whoami"], SERVER),
|
|
46
46
|
command("auth.logout", "Clear local credentials and cached agent state.", [], ["ssobig-writer auth logout"]),
|
|
47
47
|
command("agent.start", "Start the private local Writer daemon.", [], ["ssobig-writer agent start"], SERVER),
|
|
48
48
|
command("agent.status", "Show local daemon status.", [], ["ssobig-writer agent status"]),
|
|
49
49
|
command("agent.stop", "Stop the local daemon.", [], ["ssobig-writer agent stop"]),
|
|
50
|
-
command("agent.get", "Read
|
|
51
|
-
command("agent.apply", "Apply an agent-produced reviewed plan.", [option("plan", "<plan-file>", "Reviewed plan path.", { required: true }), option("receipt", "<receipt-file>", "Receipt output path.", { required: true })], ["ssobig-writer agent apply --plan plan.json --receipt receipt.json"], { ...SERVER, ...MUTATE }),
|
|
50
|
+
command("agent.get", "Read a matched scalar from the authoritative server by search match identifier.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("max-characters", "<100-20000>", "Maximum returned characters.")], ["ssobig-writer agent get --match <match-id>"], SERVER),
|
|
51
|
+
command("agent.apply", "Apply an agent-produced reviewed plan.", [option("plan", "<plan-file>", "Reviewed plan path.", { required: true }), option("receipt", "<receipt-file>", "Receipt output path.", { required: true })], ["ssobig-writer agent apply --plan .ssobig-writer/plan.json --receipt .ssobig-writer/receipt.json"], { ...SERVER, ...MUTATE }),
|
|
52
52
|
command("agent.purge", "Purge local manuscript indexes.", [], ["ssobig-writer agent purge"]),
|
|
53
53
|
command("sync", "Refresh the local manuscript index.", [], ["ssobig-writer sync"], SERVER),
|
|
54
|
-
command("read", "Read indexed
|
|
55
|
-
command("search", "Search Component values and paths.", [option("query", "<text>", "Text query."), option("value-json", "<json-scalar>", "Typed exact value."), option("mode", "<all|exact|substring|regex>", "Search mode."), option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("path", "<json-pointer>", "Exact JSON pointer."), option("path-prefix", "<json-pointer>", "JSON pointer prefix."), option("path-glob", "<glob>", "JSON pointer glob."), option("field", "<field>", "Field selector."), option("value-type", "<type>", "Value type."), option("limit", "<n>", "Maximum matches."), option("flags", "<regex-flags>", "Regular-expression flags."), option("case-sensitive", null, "Use case-sensitive matching.", { type: "boolean" }), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "
|
|
54
|
+
command("read", "Read local indexed Component data (not authoritative).", [option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("pointer", "<json-pointer>", "Read a JSON pointer."), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Sync the corpus before reading the local index; output stays non-authoritative.", { type: "boolean" }), option("require-fresh", null, "Refresh a missing or known-stale index; not a server read on every call.", { type: "boolean" })], ["ssobig-writer read --project <slug> --component <template-id> --fresh"], SERVER),
|
|
55
|
+
command("search", "Search Component values and paths.", [option("query", "<text>", "Text query."), option("value-json", "<json-scalar>", "Typed exact value."), option("mode", "<all|exact|substring|regex>", "Search mode."), option("project", "<slug|uuid>", "Limit to a project."), version, option("component", "<id|template-id>", "Limit to a Component."), option("path", "<json-pointer>", "Exact JSON pointer."), option("path-prefix", "<json-pointer>", "JSON pointer prefix."), option("path-glob", "<glob>", "JSON pointer glob."), option("field", "<field>", "Field selector."), option("value-type", "<type>", "Value type."), option("limit", "<n>", "Maximum matches."), option("flags", "<regex-flags>", "Regular-expression flags."), option("case-sensitive", null, "Use case-sensitive matching.", { type: "boolean" }), option("include-assets", null, "Include asset metadata.", { type: "boolean" }), option("fresh", null, "Sync the corpus before reading the local index; output stays non-authoritative.", { type: "boolean" }), option("require-fresh", null, "Refresh a missing or known-stale index; not a server read on every call.", { type: "boolean" })], ["ssobig-writer search --query <text> --project <slug>", "ssobig-writer search --mode all --path-prefix /characters"], SERVER),
|
|
56
56
|
command("project.list", "List visible Writer projects.", [option("status", "<active|archived>", "Project status.")], ["ssobig-writer project list --status active"], SERVER),
|
|
57
57
|
command("project.show", "Show project identity and versions.", [project, option("status", "<active|archived>", "Project status.")], ["ssobig-writer project show --project <slug>"], SERVER),
|
|
58
|
-
command("project.plan-create", "Create a reviewed project creation plan.", [option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-create --slug new-case --title 'New Case' --out plan.json"], { ...SERVER, ...PLAN }),
|
|
59
|
-
command("project.plan-import", "Create a reviewed import plan from a supported case export.", [option("source-root", "<case-directory>", "Source case root.", { required: true }), option("source-json", "<room-export.json>", "Source room export.", { required: true }), option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-import --source-root case --source-json room.json --slug new-case --title 'New Case' --out plan.json"], { ...SERVER, ...PLAN }),
|
|
60
|
-
command("project.plan-archive", "Create a reviewed project archive plan.", [project, out], ["ssobig-writer project plan-archive --project <slug> --out plan.json"], { ...SERVER, ...PLAN }),
|
|
61
|
-
command("project.plan-restore", "Create a reviewed project restore plan.", [project, out], ["ssobig-writer project plan-restore --project <slug> --out plan.json"], { ...SERVER, ...PLAN }),
|
|
62
|
-
command("project.plan-set-catalog-demo", "Create a reviewed catalog-demo status plan.", [project, option("enabled", "<true|false>", "Catalog demo state.", { required: true }), out], ["ssobig-writer project plan-set-catalog-demo --project <slug> --enabled true --out plan.json"], { ...SERVER, ...PLAN }),
|
|
58
|
+
command("project.plan-create", "Create a reviewed project creation plan.", [option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-create --slug new-case --title 'New Case' --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
59
|
+
command("project.plan-import", "Create a reviewed import plan from a supported case export.", [option("source-root", "<case-directory>", "Source case root.", { required: true }), option("source-json", "<room-export.json>", "Source room export.", { required: true }), option("slug", "<slug>", "New project slug.", { required: true }), option("title", "<title>", "Project title.", { required: true }), out], ["ssobig-writer project plan-import --source-root case --source-json room.json --slug new-case --title 'New Case' --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
60
|
+
command("project.plan-archive", "Create a reviewed project archive plan.", [project, out], ["ssobig-writer project plan-archive --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
61
|
+
command("project.plan-restore", "Create a reviewed project restore plan.", [project, out], ["ssobig-writer project plan-restore --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
62
|
+
command("project.plan-set-catalog-demo", "Create a reviewed catalog-demo status plan.", [project, option("enabled", "<true|false>", "Catalog demo state.", { required: true }), out], ["ssobig-writer project plan-set-catalog-demo --project <slug> --enabled true --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
63
63
|
command("component.list", "List Components in a workspace.", [project, version], ["ssobig-writer component list --project <slug>"], SERVER),
|
|
64
64
|
command("component.get", "Read one authoritative Component Instance.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true })], ["ssobig-writer component get --project <slug> --instance <template-id>"], SERVER),
|
|
65
|
-
command("component.plan-add", "Create a reviewed plan to add one registered optional Component.", [project, version, option("template", "<template-id>", "Registered optional Data Component Template.", { required: true }), option("instance", "<instance-id>", "Instance identifier; required for multi-instance Templates."), option("label", "<tab-label>", "User-facing Component label."), out], ["ssobig-writer component plan-add --project <slug> --template ssobig.timeline --out plan.json"], { ...SERVER, ...PLAN }),
|
|
66
|
-
command("component.plan-set-active", "Create a reviewed plan to toggle one optional Component.", [project, version, option("instance", "<instance-id>", "Stored optional Component Instance.", { required: true }), option("active", "<true|false>", "Use or archive the Component.", { required: true }), out], ["ssobig-writer component plan-set-active --project <slug> --instance clues --active false --out plan.json"], { ...SERVER, ...PLAN }),
|
|
67
|
-
command("component.plan-set-field", "Create a reviewed plan to toggle one optional Component field.", [project, version, option("instance", "<id|template-id>", "Active Component Instance or Template.", { required: true }), option("field", "<field-key>", "Registered optional field key.", { required: true }), option("enabled", "<true|false>", "Show or hide the optional field.", { required: true }), out, message], ["ssobig-writer component plan-set-field --project <slug> --instance clues --field image --enabled false --out plan.json"], { ...SERVER, ...PLAN }),
|
|
68
|
-
command("component.plan-patch", "Create a reviewed Component data patch plan.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true }), option("patch", "<file>", "JSON patch document.", { required: true }), out, message], ["ssobig-writer component plan-patch --project <slug> --instance <id> --patch patch.json --out plan.json"], { ...SERVER, ...PLAN }),
|
|
69
|
-
command("component.plan-layout-investigation-board", "Create a deterministic investigation-board layout plan.", [project, version, option("instance", "<id|template-id>", "Investigation board Component."), option("spec", "<layout-spec.json>", "
|
|
70
|
-
command("component.plan-
|
|
65
|
+
command("component.plan-add", "Create a reviewed plan to add one registered optional Component.", [project, version, option("template", "<template-id>", "Registered optional Data Component Template.", { required: true }), option("instance", "<instance-id>", "Instance identifier; required for multi-instance Templates."), option("label", "<tab-label>", "User-facing Component label."), out], ["ssobig-writer component plan-add --project <slug> --template ssobig.timeline --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
66
|
+
command("component.plan-set-active", "Create a reviewed plan to toggle one optional Component.", [project, version, option("instance", "<instance-id>", "Stored optional Component Instance.", { required: true }), option("active", "<true|false>", "Use or archive the Component.", { required: true }), out], ["ssobig-writer component plan-set-active --project <slug> --instance clues --active false --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
67
|
+
command("component.plan-set-field", "Create a reviewed plan to toggle one optional Component field.", [project, version, option("instance", "<id|template-id>", "Active Component Instance or Template.", { required: true }), option("field", "<field-key>", "Registered optional field key.", { required: true }), option("enabled", "<true|false>", "Show or hide the optional field.", { required: true }), out, message], ["ssobig-writer component plan-set-field --project <slug> --instance clues --field image --enabled false --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
68
|
+
command("component.plan-patch", "Create a reviewed Component data patch plan.", [project, version, option("instance", "<id|template-id>", "Component Instance or Template identifier.", { required: true }), option("patch", "<file>", "JSON patch document.", { required: true }), out, message], ["ssobig-writer component plan-patch --project <slug> --instance <id> --patch .ssobig-writer/patch.json --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
69
|
+
command("component.plan-layout-investigation-board", "Create a deterministic investigation-board layout plan.", [project, version, option("instance", "<id|template-id>", "Investigation board Component."), option("spec", "<layout-spec.json>", "Semantic layout spec; see the installed ssobig-writer-cli skill references/layout-spec.md (bundled at tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md).", { required: true }), out, message], ["ssobig-writer component plan-layout-investigation-board --project <slug> --spec .ssobig-writer/layout.json --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
70
|
+
command("component.plan-migrate-timeline", "Create a reviewed timeline v2 migration plan.", [project, version, option("instance", "<id|template-id>", "Timeline Component."), out, message], ["ssobig-writer component plan-migrate-timeline --project <slug> --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
71
|
+
command("component.plan-replace", "Create a reviewed indexed text replacement plan.", [option("match", "<match-id>", "Search match identifier.", { required: true }), option("old", "<text>", "Expected old text.", { required: true }), option("new", "<text>", "Replacement text.", { required: true }), out, option("occurrence", "<zero-based-index>", "Occurrence index."), message], ["ssobig-writer component plan-replace --match <id> --old old --new new --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
71
72
|
command("asset.list", "List immutable asset pointers.", [project, version], ["ssobig-writer asset list --project <slug>"], SERVER),
|
|
72
|
-
command("asset.plan-upload", "Create a reviewed asset upload plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), option("file", "<path>", "JPEG, PNG, WebP, GIF, or JSON file.", { required: true }), out, message], ["ssobig-writer asset plan-upload --project <slug> --asset-id cover --file cover.webp --out plan.json"], { ...SERVER, ...PLAN }),
|
|
73
|
-
command("asset.plan-delete", "Create a reviewed asset delete plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), out, message], ["ssobig-writer asset plan-delete --project <slug> --asset-id cover --out plan.json"], { ...SERVER, ...PLAN }),
|
|
73
|
+
command("asset.plan-upload", "Create a reviewed asset upload plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), option("file", "<path>", "JPEG, PNG, WebP, GIF, or JSON file.", { required: true }), out, message], ["ssobig-writer asset plan-upload --project <slug> --asset-id cover --file cover.webp --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
74
|
+
command("asset.plan-delete", "Create a reviewed asset delete plan.", [project, version, option("asset-id", "<id>", "Asset identifier.", { required: true }), out, message], ["ssobig-writer asset plan-delete --project <slug> --asset-id cover --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
74
75
|
command("checkpoint.list", "List workspace checkpoints.", [project, version, option("before", "<checkpoint-number>", "Pagination cursor."), option("limit", "<1-100>", "Maximum rows.")], ["ssobig-writer checkpoint list --project <slug> --limit 20"], SERVER),
|
|
75
76
|
command("checkpoint.status", "Show the current checkpoint head.", [project, version], ["ssobig-writer checkpoint status --project <slug>"], SERVER),
|
|
76
77
|
command("checkpoint.show", "Show one checkpoint snapshot.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("include-data", null, "Include manuscript data.", { type: "boolean" })], ["ssobig-writer checkpoint show --project <slug> --checkpoint 3"], SERVER),
|
|
77
78
|
command("checkpoint.diff", "Diff two checkpoints or Latest.", [project, version, option("from", "<id|number|latest>", "Diff base.", { required: true }), option("to", "<id|number|latest>", "Diff target.", { required: true }), option("include-values", null, "Include changed values.", { type: "boolean" })], ["ssobig-writer checkpoint diff --project <slug> --from 2 --to latest"], SERVER),
|
|
78
|
-
command("checkpoint.plan-create", "Create a reviewed checkpoint creation plan.", [project, version, option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-create --project <slug> --message review --out plan.json"], { ...SERVER, ...PLAN }),
|
|
79
|
-
command("checkpoint.plan-restore", "Create a reviewed checkpoint restore plan.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-restore --project <slug> --checkpoint 3 --message restore --out plan.json"], { ...SERVER, ...PLAN }),
|
|
79
|
+
command("checkpoint.plan-create", "Create a reviewed checkpoint creation plan.", [project, version, option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-create --project <slug> --message review --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
80
|
+
command("checkpoint.plan-restore", "Create a reviewed checkpoint restore plan.", [project, version, option("checkpoint", "<id|number>", "Checkpoint selector.", { required: true }), option("message", "<message>", "Checkpoint message.", { required: true }), out], ["ssobig-writer checkpoint plan-restore --project <slug> --checkpoint 3 --message restore --out .ssobig-writer/plan.json"], { ...SERVER, ...PLAN }),
|
|
80
81
|
command("validate", "Validate the authoritative Component composition.", [project, version], ["ssobig-writer validate --project <slug>"], SERVER),
|
|
81
|
-
command("apply", "Apply one reviewed plan and optionally write a receipt.", [positional("plan-file", "Reviewed plan file."), option("receipt", "<receipt-file>", "Private receipt output path.")], ["ssobig-writer apply plan.json --receipt receipt.json"], { ...SERVER, ...MUTATE }),
|
|
82
|
+
command("apply", "Apply one reviewed plan and optionally write a receipt.", [positional("plan-file", "Reviewed plan file."), option("receipt", "<receipt-file>", "Private receipt output path.")], ["ssobig-writer apply .ssobig-writer/plan.json --receipt .ssobig-writer/receipt.json"], { ...SERVER, ...MUTATE }),
|
|
82
83
|
command("skills.install", "Install the bundled project-local agent skill.", [option("target", "<workspace>", "Workspace root.", { required: true }), option("codex", null, "Install only to .agents/skills.", { type: "boolean" }), option("claude", null, "Install only to .claude/skills.", { type: "boolean" }), option("dry-run", null, "Report without changing files.", { type: "boolean" })], ["ssobig-writer skills install --target . --dry-run"]),
|
|
83
84
|
command("skills.status", "Inspect bundled skill installation state.", [option("target", "<workspace>", "Workspace root.", { required: true })], ["ssobig-writer skills status --target ."]),
|
|
84
85
|
command("skills.update", "Safely update managed skill files.", [option("target", "<workspace>", "Workspace root.", { required: true }), option("codex", null, "Update only .agents/skills.", { type: "boolean" }), option("claude", null, "Update only .claude/skills.", { type: "boolean" }), option("dry-run", null, "Report without changing files.", { type: "boolean" })], ["ssobig-writer skills update --target . --dry-run"]),
|
|
@@ -89,7 +90,7 @@ const BY_PATH = new Map(COMMANDS.map(item => [item.path, item]));
|
|
|
89
90
|
if (BY_PATH.size !== COMMANDS.length) throw new Error("Writer CLI command registry contains duplicate paths.");
|
|
90
91
|
|
|
91
92
|
const GROUP_DESCRIPTIONS = Object.freeze({
|
|
92
|
-
version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "
|
|
93
|
+
version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "Writer member authentication", agent: "local agent backend",
|
|
93
94
|
sync: "local index refresh", read: "targeted manuscript read", search: "targeted manuscript search", project: "project identity and plans",
|
|
94
95
|
component: "Component data and plans", asset: "asset pointers and plans", checkpoint: "workspace history", validate: "composition validation",
|
|
95
96
|
apply: "reviewed plan mutation", skills: "project-local agent skills"
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
createComponentFieldPlan,
|
|
21
21
|
createComponentPlan,
|
|
22
22
|
createInvestigationBoardLayoutPlan,
|
|
23
|
+
createTimelineMigrationPlan,
|
|
23
24
|
createAssetPlan,
|
|
24
25
|
createProjectCreationPlan,
|
|
25
26
|
createProjectImportPlan,
|
|
@@ -648,7 +649,7 @@ async function execute(argv, deps) {
|
|
|
648
649
|
return { command: "project.plan-set-catalog-demo", data: { planFile: outputPath, planId: plan.planId, digest: plan.digest, operation: plan.operation, project: plan.project, target: plan.target } };
|
|
649
650
|
}
|
|
650
651
|
|
|
651
|
-
if (command === "component" && ["list", "get", "plan-add", "plan-set-active", "plan-set-field", "plan-patch", "plan-layout-investigation-board"].includes(subcommand)) {
|
|
652
|
+
if (command === "component" && ["list", "get", "plan-add", "plan-set-active", "plan-set-field", "plan-patch", "plan-layout-investigation-board", "plan-migrate-timeline"].includes(subcommand)) {
|
|
652
653
|
assertAllowedOptions(options, `component.${subcommand}`);
|
|
653
654
|
const projectRef = requireOption(options, "project");
|
|
654
655
|
const validated = validateLoadedVersion(await gateway.loadLatestVersion(projectRef, optionalVersionOption(options)));
|
|
@@ -725,6 +726,26 @@ async function execute(argv, deps) {
|
|
|
725
726
|
}
|
|
726
727
|
};
|
|
727
728
|
}
|
|
729
|
+
if (subcommand === "plan-migrate-timeline") {
|
|
730
|
+
const timelinePlanOptions = planOptions(deps);
|
|
731
|
+
if (options.message !== undefined) timelinePlanOptions.checkpointMessage = String(options.message);
|
|
732
|
+
const plan = createTimelineMigrationPlan(validated, options.instance || "timeline", user.email, timelinePlanOptions);
|
|
733
|
+
const outputPath = writePrivateJson(requireOption(options, "out"), plan, deps.fs);
|
|
734
|
+
return {
|
|
735
|
+
command: "component.plan-migrate-timeline",
|
|
736
|
+
data: {
|
|
737
|
+
planFile: outputPath,
|
|
738
|
+
planId: plan.planId,
|
|
739
|
+
digest: plan.digest,
|
|
740
|
+
operation: plan.operation,
|
|
741
|
+
checkpointMessage: plan.checkpointMessage,
|
|
742
|
+
target: plan.target,
|
|
743
|
+
migration: plan.migration,
|
|
744
|
+
beforeHash: plan.beforeHash,
|
|
745
|
+
afterHash: plan.afterHash
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
}
|
|
728
749
|
const selector = requireOption(options, "instance");
|
|
729
750
|
if (subcommand === "get") {
|
|
730
751
|
return { command: "component.get", data: { project: publicProject(validated.project, validated.instances), versionNumber: version, component: publicInstance(findInstance(validated, selector), true) } };
|
|
@@ -5,6 +5,7 @@ const componentContract = require("../../../templates/mystery-v1/component-stora
|
|
|
5
5
|
const componentCatalog = require("../../../templates/mystery-v1/component-catalog-contract.js");
|
|
6
6
|
const componentRegistry = require("../../../templates/mystery-v1/component-registry.js").createDefaultRegistry();
|
|
7
7
|
const viewContract = require("../../../templates/mystery-v1/view-component-contract.js");
|
|
8
|
+
const timelineModel = require("../../../templates/mystery-v1/timeline-model.js");
|
|
8
9
|
const projectRuntime = require("../../../project-runtime.js");
|
|
9
10
|
const { applyPatch } = require("./json-patch.cjs");
|
|
10
11
|
const { clone, equalJson, sha256 } = require("./json.cjs");
|
|
@@ -399,6 +400,50 @@ function createInvestigationBoardLayoutPlan(validated, selector, spec, actorEmai
|
|
|
399
400
|
return finalizePlan(plan);
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
function timelineEntryCountsByCharacter(entries) {
|
|
404
|
+
const counts = new Map();
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (entry.lane?.kind !== "character") continue;
|
|
407
|
+
const characterId = String(entry.lane.characterId || "");
|
|
408
|
+
counts.set(characterId, (counts.get(characterId) || 0) + 1);
|
|
409
|
+
}
|
|
410
|
+
return Object.fromEntries([...counts.keys()].sort().map(characterId => [characterId, counts.get(characterId)]));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function createTimelineMigrationPlan(validated, selector, actorEmail, options = {}) {
|
|
414
|
+
const instance = findInstance(validated, selector || "timeline");
|
|
415
|
+
if (instance.templateId !== "ssobig.timeline") {
|
|
416
|
+
throw cliError("E_VALIDATION", "타임라인 v2 이관은 ssobig.timeline Component에만 사용할 수 있습니다.");
|
|
417
|
+
}
|
|
418
|
+
const shape = timelineModel.detectShape(instance.data);
|
|
419
|
+
if (shape === "v2") throw cliError("E_NO_CHANGES", "타임라인 Component가 이미 v2 모양입니다.");
|
|
420
|
+
if (shape !== "legacy") throw cliError("E_VALIDATION", "타임라인 Component data가 legacy 또는 v2 모양이 아닙니다.");
|
|
421
|
+
const converted = timelineModel.convertLegacyTimeline(instance.data, { idFactory: options.idFactory });
|
|
422
|
+
const review = timelineModel.compareLegacyToV2(instance.data, converted);
|
|
423
|
+
if (!review.ok) throw cliError("E_VALIDATION", `타임라인 v2 변환이 원본과 일치하지 않습니다: ${review.mismatches.join(" / ")}`);
|
|
424
|
+
const patch = [
|
|
425
|
+
{ op: "test", path: "/events", value: clone(instance.data.events) },
|
|
426
|
+
{ op: "remove", path: "/events" },
|
|
427
|
+
{ op: "add", path: "/timeLevels", value: clone(converted.timeLevels) },
|
|
428
|
+
{ op: "add", path: "/entries", value: clone(converted.entries) }
|
|
429
|
+
];
|
|
430
|
+
const componentPlan = createComponentPlan(validated, instance.instanceId, patch, actorEmail, {
|
|
431
|
+
...options,
|
|
432
|
+
checkpointMessage: options.checkpointMessage || "CLI: 타임라인 v2 이관"
|
|
433
|
+
});
|
|
434
|
+
const plan = clone(componentPlan);
|
|
435
|
+
delete plan.digest;
|
|
436
|
+
plan.migration = {
|
|
437
|
+
kind: "timeline-v2",
|
|
438
|
+
counts: clone(review.counts),
|
|
439
|
+
mismatches: clone(review.mismatches),
|
|
440
|
+
levelCount: converted.timeLevels.length,
|
|
441
|
+
entryCount: converted.entries.length,
|
|
442
|
+
entryCountByCharacter: timelineEntryCountsByCharacter(converted.entries)
|
|
443
|
+
};
|
|
444
|
+
return finalizePlan(plan);
|
|
445
|
+
}
|
|
446
|
+
|
|
402
447
|
function createAssetPlan(validated, operation, assetId, actorEmail, options = {}) {
|
|
403
448
|
const assetInstance = componentContract.assetInstance(validated.instances);
|
|
404
449
|
const manifest = componentContract.assetManifest(validated.instances);
|
|
@@ -959,6 +1004,7 @@ module.exports = Object.freeze({
|
|
|
959
1004
|
createComponentPlan,
|
|
960
1005
|
investigationBoardReferences,
|
|
961
1006
|
createInvestigationBoardLayoutPlan,
|
|
1007
|
+
createTimelineMigrationPlan,
|
|
962
1008
|
createAssetPlan,
|
|
963
1009
|
createProjectCreationPlan,
|
|
964
1010
|
createProjectImportPlan,
|
|
@@ -6,6 +6,7 @@ const path = require("node:path");
|
|
|
6
6
|
const catalogContract = require("../../../templates/mystery-v1/component-catalog-contract.js");
|
|
7
7
|
const componentContract = require("../../../templates/mystery-v1/component-storage-contract.js");
|
|
8
8
|
const viewContract = require("../../../templates/mystery-v1/view-component-contract.js");
|
|
9
|
+
const timelineModel = require("../../../templates/mystery-v1/timeline-model.js");
|
|
9
10
|
const { inspectAssetFile } = require("./asset-policy.cjs");
|
|
10
11
|
const { clone, equalJson, sha256 } = require("./json.cjs");
|
|
11
12
|
const { cliError } = require("./errors.cjs");
|
|
@@ -142,6 +143,15 @@ function parseTimeline(source) {
|
|
|
142
143
|
return events;
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
// 원본 표는 사건이 시간을 소유하는 legacy 행으로 읽고, 등록된 v2 모양으로만 Component data를 만든다.
|
|
147
|
+
function timelineComponentData(events) {
|
|
148
|
+
const legacy = { events };
|
|
149
|
+
const converted = timelineModel.convertLegacyTimeline(legacy);
|
|
150
|
+
const review = timelineModel.compareLegacyToV2(legacy, converted);
|
|
151
|
+
if (!review.ok) throw cliError("E_VALIDATION", `타임라인 v2 변환이 원본과 일치하지 않습니다: ${review.mismatches.join(" / ")}`);
|
|
152
|
+
return { data: converted, review };
|
|
153
|
+
}
|
|
154
|
+
|
|
145
155
|
function characterComponent(characterSource) {
|
|
146
156
|
const characters = {};
|
|
147
157
|
const names = {};
|
|
@@ -354,6 +364,7 @@ function loadTrueWriterCase(input, options = {}) {
|
|
|
354
364
|
const progress = progressComponent(jsonData.game_set.flow);
|
|
355
365
|
const ending = endingComponent(jsonData.game_set.ending);
|
|
356
366
|
const timelineEvents = parseTimeline(docs["03-timeline.md"]);
|
|
367
|
+
const timeline = timelineComponentData(timelineEvents);
|
|
357
368
|
const resources = Object.keys(jsonData.resource).length;
|
|
358
369
|
const components = normalizeImportComponents([
|
|
359
370
|
component("basic", {
|
|
@@ -380,7 +391,7 @@ function loadTrueWriterCase(input, options = {}) {
|
|
|
380
391
|
component("characters", characterComponent(jsonData.game_set.character)),
|
|
381
392
|
component("assets", { assets: {} }),
|
|
382
393
|
component("clues", cluesComponent(jsonData.resource)),
|
|
383
|
-
component("timeline",
|
|
394
|
+
component("timeline", timeline.data),
|
|
384
395
|
component("ending", ending),
|
|
385
396
|
component("author-notes", {
|
|
386
397
|
notes: [
|
|
@@ -417,6 +428,9 @@ function loadTrueWriterCase(input, options = {}) {
|
|
|
417
428
|
endingGroups: Object.keys(jsonData.game_set.ending || {}).length,
|
|
418
429
|
endingOutcomes: ending.outcomes.length,
|
|
419
430
|
timelineEvents: timelineEvents.length,
|
|
431
|
+
timelineLevels: timeline.data.timeLevels.length,
|
|
432
|
+
timelineEntries: timeline.data.entries.length,
|
|
433
|
+
timelineConversion: { shape: "v2", lossless: timeline.review.ok, ...timeline.review.counts },
|
|
420
434
|
components: components.length,
|
|
421
435
|
assets: assetFiles.length,
|
|
422
436
|
unexplained: 0
|