@ssobig/writer-cli 0.2.2 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +19 -0
  2. package/asset-repository.js +7 -2
  3. package/config.js +2 -1
  4. package/package.json +1 -1
  5. package/templates/mystery-v1/authoring-view-preference.js +22 -5
  6. package/templates/mystery-v1/character-perspective-preview.js +3 -3
  7. package/templates/mystery-v1/codemirror6-runtime.min.js +28 -0
  8. package/templates/mystery-v1/component-asset-operations.js +19 -6
  9. package/templates/mystery-v1/component-catalog-contract.js +26 -35
  10. package/templates/mystery-v1/component-draft-operations.js +19 -8
  11. package/templates/mystery-v1/component-field-contracts.js +127 -51
  12. package/templates/mystery-v1/component-id-policy.js +1 -1
  13. package/templates/mystery-v1/component-manager.js +21 -11
  14. package/templates/mystery-v1/component-navigation-counts.js +9 -9
  15. package/templates/mystery-v1/component-registry.js +11 -5
  16. package/templates/mystery-v1/component-renderers.js +12 -8
  17. package/templates/mystery-v1/component-storage-contract.js +28 -9
  18. package/templates/mystery-v1/markdown-document-model.js +444 -0
  19. package/templates/mystery-v1/markdown-image-editor.js +320 -0
  20. package/templates/mystery-v1/markdown-live-editor.js +1224 -0
  21. package/templates/mystery-v1/page-header.js +2 -1
  22. package/templates/mystery-v1/timeline-model.js +235 -0
  23. package/tools/writer-cli/package-lock.json +2 -2
  24. package/tools/writer-cli/package.json +1 -1
  25. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +13 -14
  26. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -1
  27. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +4 -1
  28. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +3 -3
  29. package/tools/writer-cli/skills/ssobig-writer-cli/references/investigation-board.md +9 -0
  30. package/tools/writer-cli/skills/ssobig-writer-cli/references/layout-spec.md +130 -0
  31. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +8 -2
  32. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +12 -2
  33. package/tools/writer-cli/src/agent-service.cjs +3 -1
  34. package/tools/writer-cli/src/command-registry.cjs +24 -20
  35. package/tools/writer-cli/src/commands.cjs +106 -3
  36. package/tools/writer-cli/src/domain.cjs +296 -1
  37. package/tools/writer-cli/src/gateway.cjs +14 -0
  38. package/tools/writer-cli/src/mutations.cjs +167 -1
  39. package/tools/writer-cli/src/project-import.cjs +27 -22
@@ -41,41 +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 staff authentication 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 staff identity.", [], ["ssobig-writer auth whoami"], SERVER),
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 one indexed match by 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 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 or authoritative Component data.", [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, "Prefer an authoritative refresh.", { type: "boolean" }), option("require-fresh", null, "Fail unless authoritative data is available.", { 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, "Prefer an authoritative refresh.", { type: "boolean" }), option("require-fresh", null, "Fail unless authoritative data is available.", { type: "boolean" })], ["ssobig-writer search --query <text> --project <slug>", "ssobig-writer search --mode all --path-prefix /characters"], SERVER),
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-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 }),
66
- 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>", "Validated layout spec.", { required: true }), out, message], ["ssobig-writer component plan-layout-investigation-board --project <slug> --spec layout.json --out plan.json"], { ...SERVER, ...PLAN }),
67
- 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 plan.json"], { ...SERVER, ...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 }),
68
72
  command("asset.list", "List immutable asset pointers.", [project, version], ["ssobig-writer asset list --project <slug>"], SERVER),
69
- 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 }),
70
- 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 }),
71
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),
72
76
  command("checkpoint.status", "Show the current checkpoint head.", [project, version], ["ssobig-writer checkpoint status --project <slug>"], SERVER),
73
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),
74
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),
75
- 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 }),
76
- 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 }),
77
81
  command("validate", "Validate the authoritative Component composition.", [project, version], ["ssobig-writer validate --project <slug>"], SERVER),
78
- 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 }),
79
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"]),
80
84
  command("skills.status", "Inspect bundled skill installation state.", [option("target", "<workspace>", "Workspace root.", { required: true })], ["ssobig-writer skills status --target ."]),
81
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"]),
@@ -86,7 +90,7 @@ const BY_PATH = new Map(COMMANDS.map(item => [item.path, item]));
86
90
  if (BY_PATH.size !== COMMANDS.length) throw new Error("Writer CLI command registry contains duplicate paths.");
87
91
 
88
92
  const GROUP_DESCRIPTIONS = Object.freeze({
89
- version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "staff authentication", agent: "local agent backend",
93
+ version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "Writer member authentication", agent: "local agent backend",
90
94
  sync: "local index refresh", read: "targeted manuscript read", search: "targeted manuscript search", project: "project identity and plans",
91
95
  component: "Component data and plans", asset: "asset pointers and plans", checkpoint: "workspace history", validate: "composition validation",
92
96
  apply: "reviewed plan mutation", skills: "project-local agent skills"
@@ -5,6 +5,8 @@ const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const config = require("../../../config.js");
7
7
  const projectRuntime = require("../../../project-runtime.js");
8
+ const componentCatalog = require("../../../templates/mystery-v1/component-catalog-contract.js");
9
+ const componentRegistry = require("../../../templates/mystery-v1/component-registry.js").createDefaultRegistry();
8
10
  const { createAuthManager } = require("./auth.cjs");
9
11
  const { createSupabaseGateway } = require("./gateway.cjs");
10
12
  const { inspectAssetFile, assertAssetId } = require("./asset-policy.cjs");
@@ -13,8 +15,12 @@ const {
13
15
  componentContract,
14
16
  validateLoadedVersion,
15
17
  findInstance,
18
+ createComponentActivePlan,
19
+ createComponentAddPlan,
20
+ createComponentFieldPlan,
16
21
  createComponentPlan,
17
22
  createInvestigationBoardLayoutPlan,
23
+ createTimelineMigrationPlan,
18
24
  createAssetPlan,
19
25
  createProjectCreationPlan,
20
26
  createProjectImportPlan,
@@ -25,7 +31,7 @@ const {
25
31
  verifyPlan
26
32
  } = require("./domain.cjs");
27
33
  const { loadTrueWriterCase } = require("./project-import.cjs");
28
- const { applyProjectCreationPlan, applyProjectImportPlan, applyProjectStatusPlan, applyCatalogDemoPlan, applyComponentPlan, applyAssetPlan, applyCheckpointPlan } = require("./mutations.cjs");
34
+ const { applyProjectCreationPlan, applyProjectImportPlan, applyProjectStatusPlan, applyCatalogDemoPlan, applyComponentActivePlan, applyComponentAddPlan, applyComponentPlan, applyAssetPlan, applyCheckpointPlan } = require("./mutations.cjs");
29
35
  const { snapshotSummary, diffSnapshots } = require("./checkpoint-diff.cjs");
30
36
  const { cliError, normalizeError, redact } = require("./errors.cjs");
31
37
  const { startDaemon, stopDaemon } = require("./daemon-runner.cjs");
@@ -186,10 +192,43 @@ function publicInstance(instance, includeData = false) {
186
192
  required: instance.required,
187
193
  removable: instance.removable
188
194
  };
195
+ const fields = componentRegistry.get(instance.templateId)?.fieldConfiguration || [];
196
+ if (fields.length) {
197
+ const hasSelection = Array.isArray(instance.data?.enabledOptionalFields);
198
+ const enabled = new Set(hasSelection ? instance.data.enabledOptionalFields : fields.filter(field => !field.required).map(field => field.key));
199
+ result.fieldConfiguration = fields.map(field => ({
200
+ key: field.key,
201
+ label: field.label,
202
+ description: field.description,
203
+ required: field.required,
204
+ enabled: field.required || enabled.has(field.key)
205
+ }));
206
+ }
189
207
  if (includeData) result.data = instance.data;
190
208
  return result;
191
209
  }
192
210
 
211
+ function publicComponentAvailability(validated) {
212
+ const archivedComponents = componentContract.archivedInstances(validated.componentRows);
213
+ const occupiedSingletonTemplates = new Set(validated.componentRows.map(row => String(row.template_id)));
214
+ const addableTemplates = componentCatalog.list()
215
+ .filter(template => !template.required && !template.internal && template.view
216
+ && (template.allowMultiple || !occupiedSingletonTemplates.has(template.templateId)))
217
+ .map(template => ({
218
+ templateId: template.templateId,
219
+ defaultInstanceId: template.defaultInstanceId,
220
+ tabLabel: template.tabLabel,
221
+ displayLabel: template.displayLabel,
222
+ description: template.description,
223
+ allowMultiple: template.allowMultiple,
224
+ requiresInstanceId: template.allowMultiple
225
+ }));
226
+ return {
227
+ archivedComponents: archivedComponents.map(item => publicInstance(item)),
228
+ addableTemplates
229
+ };
230
+ }
231
+
193
232
  function publicCheckpoint(row, projectId, versionNumber) {
194
233
  return normalizeCheckpoint({
195
234
  ...row,
@@ -610,13 +649,53 @@ async function execute(argv, deps) {
610
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 } };
611
650
  }
612
651
 
613
- if (command === "component" && ["list", "get", "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)) {
614
653
  assertAllowedOptions(options, `component.${subcommand}`);
615
654
  const projectRef = requireOption(options, "project");
616
655
  const validated = validateLoadedVersion(await gateway.loadLatestVersion(projectRef, optionalVersionOption(options)));
617
656
  const version = validated.versionNumber;
618
657
  if (subcommand === "list") {
619
- return { command: "component.list", data: { project: publicProject(validated.project, validated.instances), versionNumber: version, components: validated.instances.map(item => publicInstance(item)) } };
658
+ return { command: "component.list", data: { project: publicProject(validated.project, validated.instances), versionNumber: version, components: validated.instances.map(item => publicInstance(item)), ...publicComponentAvailability(validated) } };
659
+ }
660
+ if (subcommand === "plan-add") {
661
+ const plan = createComponentAddPlan(validated, {
662
+ templateId: requireOption(options, "template"),
663
+ instanceId: options.instance,
664
+ tabLabel: options.label
665
+ }, user.email, planOptions(deps));
666
+ const outputPath = writePrivateJson(requireOption(options, "out"), plan, deps.fs);
667
+ return {
668
+ command: "component.plan-add",
669
+ data: {
670
+ planFile: outputPath,
671
+ planId: plan.planId,
672
+ digest: plan.digest,
673
+ operation: plan.operation,
674
+ target: plan.target,
675
+ initialData: plan.initialData,
676
+ initialDataHash: plan.initialDataHash,
677
+ composition: plan.composition
678
+ }
679
+ };
680
+ }
681
+ if (subcommand === "plan-set-active") {
682
+ const activeValue = requireOption(options, "active");
683
+ if (!new Set(["true", "false"]).has(activeValue)) throw cliError("E_USAGE", "--active는 true 또는 false여야 합니다.");
684
+ const plan = createComponentActivePlan(validated, {
685
+ instanceId: requireOption(options, "instance"),
686
+ active: activeValue === "true"
687
+ }, user.email, planOptions(deps));
688
+ const outputPath = writePrivateJson(requireOption(options, "out"), plan, deps.fs);
689
+ return { command: "component.plan-set-active", data: { planFile: outputPath, planId: plan.planId, digest: plan.digest, operation: plan.operation, target: plan.target, composition: plan.composition } };
690
+ }
691
+ if (subcommand === "plan-set-field") {
692
+ const enabledValue = requireOption(options, "enabled");
693
+ if (!new Set(["true", "false"]).has(enabledValue)) throw cliError("E_USAGE", "--enabled는 true 또는 false여야 합니다.");
694
+ const fieldPlanOptions = planOptions(deps);
695
+ if (options.message !== undefined) fieldPlanOptions.checkpointMessage = String(options.message);
696
+ const plan = createComponentFieldPlan(validated, requireOption(options, "instance"), requireOption(options, "field"), enabledValue === "true", user.email, fieldPlanOptions);
697
+ const outputPath = writePrivateJson(requireOption(options, "out"), plan, deps.fs);
698
+ return { command: "component.plan-set-field", data: { planFile: outputPath, planId: plan.planId, digest: plan.digest, operation: plan.operation, checkpointMessage: plan.checkpointMessage, target: plan.target, fieldSelection: plan.fieldSelection, beforeHash: plan.beforeHash, afterHash: plan.afterHash } };
620
699
  }
621
700
  if (subcommand === "plan-layout-investigation-board") {
622
701
  const spec = readJson(requireOption(options, "spec"), "추리 보드 layout spec", deps.fs).value;
@@ -647,6 +726,26 @@ async function execute(argv, deps) {
647
726
  }
648
727
  };
649
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
+ }
650
749
  const selector = requireOption(options, "instance");
651
750
  if (subcommand === "get") {
652
751
  return { command: "component.get", data: { project: publicProject(validated.project, validated.instances), versionNumber: version, component: publicInstance(findInstance(validated, selector), true) } };
@@ -783,6 +882,10 @@ async function execute(argv, deps) {
783
882
  receipt = await applyCatalogDemoPlan(gateway, plan, user.email, { appliedAt: nowIso(deps) });
784
883
  } else if (plan.operation.startsWith("project.")) {
785
884
  receipt = await applyProjectStatusPlan(gateway, plan, user.email, { appliedAt: nowIso(deps) });
885
+ } else if (plan.operation === "component.set-active") {
886
+ receipt = await applyComponentActivePlan(gateway, plan, user.email, { appliedAt: nowIso(deps) });
887
+ } else if (plan.operation === "component.add") {
888
+ receipt = await applyComponentAddPlan(gateway, plan, user.email, { appliedAt: nowIso(deps) });
786
889
  } else if (plan.operation === "component.patch") {
787
890
  receipt = await applyComponentPlan(gateway, plan, user.email, { appliedAt: nowIso(deps) });
788
891
  } else if (plan.operation.startsWith("checkpoint.")) {