@ssobig/writer-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +35 -0
  2. package/asset-repository.js +278 -0
  3. package/config.js +14 -0
  4. package/package.json +28 -0
  5. package/project-runtime.js +102 -0
  6. package/storage-path.js +110 -0
  7. package/templates/mystery-v1/authoring-view-preference.js +34 -0
  8. package/templates/mystery-v1/character-perspective-preview.js +61 -0
  9. package/templates/mystery-v1/component-asset-operations.js +103 -0
  10. package/templates/mystery-v1/component-autosave.js +121 -0
  11. package/templates/mystery-v1/component-catalog-contract.js +340 -0
  12. package/templates/mystery-v1/component-checkpoint-history.js +145 -0
  13. package/templates/mystery-v1/component-contract.js +90 -0
  14. package/templates/mystery-v1/component-draft-operations.js +313 -0
  15. package/templates/mystery-v1/component-field-contracts.js +595 -0
  16. package/templates/mystery-v1/component-id-policy.js +64 -0
  17. package/templates/mystery-v1/component-manager.js +396 -0
  18. package/templates/mystery-v1/component-navigation-counts.js +64 -0
  19. package/templates/mystery-v1/component-registry.js +205 -0
  20. package/templates/mystery-v1/component-renderers.js +139 -0
  21. package/templates/mystery-v1/component-storage-contract.js +237 -0
  22. package/templates/mystery-v1/external-update-coordinator.js +91 -0
  23. package/templates/mystery-v1/output-clue-card-layout.js +46 -0
  24. package/templates/mystery-v1/page-header.js +26 -0
  25. package/templates/mystery-v1/render-ui-state.js +76 -0
  26. package/templates/mystery-v1/runtime-snapshot-reconciler.js +40 -0
  27. package/templates/mystery-v1/tab-bar.js +87 -0
  28. package/templates/mystery-v1/view-component-contract.js +152 -0
  29. package/templates/mystery-v1/view-component-registry.js +44 -0
  30. package/templates/mystery-v1/view-component-runtime.js +95 -0
  31. package/tools/writer-cli/bin/ssobig-writer-daemon.cjs +34 -0
  32. package/tools/writer-cli/bin/ssobig-writer.cjs +12 -0
  33. package/tools/writer-cli/package-lock.json +121 -0
  34. package/tools/writer-cli/package.json +22 -0
  35. package/tools/writer-cli/skills/ssobig-writer-cli/SKILL.md +38 -0
  36. package/tools/writer-cli/skills/ssobig-writer-cli/agents/openai.yaml +4 -0
  37. package/tools/writer-cli/skills/ssobig-writer-cli/references/assets-checkpoints.md +5 -0
  38. package/tools/writer-cli/skills/ssobig-writer-cli/references/errors.md +10 -0
  39. package/tools/writer-cli/skills/ssobig-writer-cli/references/install-auth.md +7 -0
  40. package/tools/writer-cli/skills/ssobig-writer-cli/references/projects-components.md +7 -0
  41. package/tools/writer-cli/skills/ssobig-writer-cli/references/read-search.md +5 -0
  42. package/tools/writer-cli/src/agent-paths.cjs +114 -0
  43. package/tools/writer-cli/src/agent-service.cjs +496 -0
  44. package/tools/writer-cli/src/asset-policy.cjs +113 -0
  45. package/tools/writer-cli/src/auth.cjs +655 -0
  46. package/tools/writer-cli/src/checkpoint-diff.cjs +128 -0
  47. package/tools/writer-cli/src/command-registry.cjs +152 -0
  48. package/tools/writer-cli/src/commands.cjs +841 -0
  49. package/tools/writer-cli/src/corpus.cjs +83 -0
  50. package/tools/writer-cli/src/daemon-app.cjs +106 -0
  51. package/tools/writer-cli/src/daemon-client.cjs +187 -0
  52. package/tools/writer-cli/src/daemon-protocol.cjs +184 -0
  53. package/tools/writer-cli/src/daemon-runner.cjs +97 -0
  54. package/tools/writer-cli/src/daemon-server.cjs +378 -0
  55. package/tools/writer-cli/src/diagnostics.cjs +235 -0
  56. package/tools/writer-cli/src/domain.cjs +731 -0
  57. package/tools/writer-cli/src/errors.cjs +47 -0
  58. package/tools/writer-cli/src/gateway.cjs +357 -0
  59. package/tools/writer-cli/src/investigation-board-layout.cjs +328 -0
  60. package/tools/writer-cli/src/json-patch.cjs +98 -0
  61. package/tools/writer-cli/src/json.cjs +26 -0
  62. package/tools/writer-cli/src/local-index-cache.cjs +139 -0
  63. package/tools/writer-cli/src/local-index-lookup.cjs +98 -0
  64. package/tools/writer-cli/src/local-index-query.cjs +304 -0
  65. package/tools/writer-cli/src/local-index-snapshot.cjs +235 -0
  66. package/tools/writer-cli/src/local-index-storage.cjs +284 -0
  67. package/tools/writer-cli/src/local-index.cjs +199 -0
  68. package/tools/writer-cli/src/mutations.cjs +722 -0
  69. package/tools/writer-cli/src/platform-runner.cjs +55 -0
  70. package/tools/writer-cli/src/project-import.cjs +485 -0
  71. package/tools/writer-cli/src/skill-manager.cjs +255 -0
  72. package/tools/writer-cli/src/source-fingerprint.cjs +90 -0
  73. package/tools/writer-cli/src/update-gate.cjs +102 -0
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+
3
+ const { equalJson } = require("./json.cjs");
4
+
5
+ function escapePointerToken(value) {
6
+ return String(value).replaceAll("~", "~0").replaceAll("/", "~1");
7
+ }
8
+
9
+ function valueType(value) {
10
+ if (value === null) return "null";
11
+ if (Array.isArray(value)) return "array";
12
+ return typeof value;
13
+ }
14
+
15
+ function appendChange(changes, operation, path, before, after, includeValues) {
16
+ const change = {
17
+ operation,
18
+ path: path || "",
19
+ beforeType: operation === "add" ? null : valueType(before),
20
+ afterType: operation === "remove" ? null : valueType(after)
21
+ };
22
+ if (includeValues) {
23
+ if (operation !== "add") change.before = structuredClone(before);
24
+ if (operation !== "remove") change.after = structuredClone(after);
25
+ }
26
+ changes.push(change);
27
+ }
28
+
29
+ function walkDiff(before, after, path, changes, includeValues) {
30
+ if (equalJson(before, after)) return;
31
+ if (Array.isArray(before) && Array.isArray(after)) {
32
+ const length = Math.max(before.length, after.length);
33
+ for (let index = 0; index < length; index += 1) {
34
+ const nextPath = `${path}/${index}`;
35
+ if (index >= before.length) appendChange(changes, "add", nextPath, undefined, after[index], includeValues);
36
+ else if (index >= after.length) appendChange(changes, "remove", nextPath, before[index], undefined, includeValues);
37
+ else walkDiff(before[index], after[index], nextPath, changes, includeValues);
38
+ }
39
+ return;
40
+ }
41
+ if (before && after && typeof before === "object" && typeof after === "object"
42
+ && !Array.isArray(before) && !Array.isArray(after)) {
43
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
44
+ for (const key of keys) {
45
+ const nextPath = `${path}/${escapePointerToken(key)}`;
46
+ if (!Object.hasOwn(before, key)) appendChange(changes, "add", nextPath, undefined, after[key], includeValues);
47
+ else if (!Object.hasOwn(after, key)) appendChange(changes, "remove", nextPath, before[key], undefined, includeValues);
48
+ else walkDiff(before[key], after[key], nextPath, changes, includeValues);
49
+ }
50
+ return;
51
+ }
52
+ appendChange(changes, "replace", path, before, after, includeValues);
53
+ }
54
+
55
+ function entrySummary(entry, includeData = false) {
56
+ const value = {
57
+ instanceId: String(entry.instance_id),
58
+ templateId: String(entry.template_id),
59
+ tabLabel: String(entry.tab_label || ""),
60
+ sortOrder: Number(entry.sort_order),
61
+ enabled: entry.is_enabled === true,
62
+ archived: entry.is_archived === true,
63
+ sourceRevision: Number(entry.source_revision ?? entry.revision),
64
+ dataHash: String(entry.data_hash || ""),
65
+ changedFromParent: entry.changed_from_parent === true
66
+ };
67
+ if (includeData) value.data = structuredClone(entry.data);
68
+ return value;
69
+ }
70
+
71
+ function snapshotSummary(snapshot, includeData = false) {
72
+ return {
73
+ checkpoint: snapshot.checkpoint,
74
+ components: snapshot.entries.map(entry => entrySummary(entry, includeData))
75
+ };
76
+ }
77
+
78
+ function diffSnapshots(before, after, options = {}) {
79
+ const includeValues = options.includeValues === true;
80
+ const beforeById = new Map(before.entries.map(entry => [String(entry.instance_id), entry]));
81
+ const afterById = new Map(after.entries.map(entry => [String(entry.instance_id), entry]));
82
+ const ids = [...new Set([...beforeById.keys(), ...afterById.keys()])].sort((left, right) => {
83
+ const leftOrder = Number(beforeById.get(left)?.sort_order ?? afterById.get(left)?.sort_order ?? 0);
84
+ const rightOrder = Number(beforeById.get(right)?.sort_order ?? afterById.get(right)?.sort_order ?? 0);
85
+ return leftOrder - rightOrder || left.localeCompare(right);
86
+ });
87
+ const components = [];
88
+ for (const instanceId of ids) {
89
+ const beforeEntry = beforeById.get(instanceId);
90
+ const afterEntry = afterById.get(instanceId);
91
+ if (!beforeEntry || !afterEntry) {
92
+ components.push({
93
+ instanceId,
94
+ templateId: String((afterEntry || beforeEntry).template_id),
95
+ change: beforeEntry ? "removed" : "added",
96
+ pathCount: 1,
97
+ paths: [{ operation: beforeEntry ? "remove" : "add", path: "" }]
98
+ });
99
+ continue;
100
+ }
101
+ const paths = [];
102
+ walkDiff(beforeEntry.data, afterEntry.data, "", paths, includeValues);
103
+ const metadataChanged = [
104
+ "template_id", "tab_label", "sort_order",
105
+ "is_enabled", "is_archived", "is_required", "is_removable", "editor_view_id",
106
+ "preview_view_id", "capabilities", "config"
107
+ ].some(key => !equalJson(beforeEntry[key], afterEntry[key]));
108
+ if (paths.length || metadataChanged) {
109
+ components.push({
110
+ instanceId,
111
+ templateId: String(afterEntry.template_id),
112
+ change: metadataChanged && !paths.length ? "metadata" : "modified",
113
+ metadataChanged,
114
+ pathCount: paths.length,
115
+ paths
116
+ });
117
+ }
118
+ }
119
+ return {
120
+ from: before.checkpoint,
121
+ to: after.checkpoint,
122
+ changedComponentCount: components.length,
123
+ changedPathCount: components.reduce((sum, item) => sum + item.pathCount, 0),
124
+ components
125
+ };
126
+ }
127
+
128
+ module.exports = Object.freeze({ entrySummary, snapshotSummary, diffSnapshots });
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+
3
+ const { cliError } = require("./errors.cjs");
4
+
5
+ const GLOBAL_INPUTS = Object.freeze([
6
+ Object.freeze({ name: "json", type: "boolean", required: false, description: "JSON output (default)." }),
7
+ Object.freeze({ name: "compact", type: "boolean", required: false, description: "Write compact JSON." }),
8
+ Object.freeze({ name: "help", type: "boolean", required: false, description: "Show scoped help." })
9
+ ]);
10
+
11
+ const option = (name, value, description, extra = {}) => Object.freeze({ name, type: extra.type || "string", required: extra.required === true, value, description, ...extra });
12
+ const positional = (name, description, extra = {}) => Object.freeze({ name, type: "positional", required: extra.required !== false, description, ...extra });
13
+
14
+ const SERVER = Object.freeze({ serverAccess: true, updatePolicy: "required" });
15
+ const LOCAL = Object.freeze({ serverAccess: false, updatePolicy: "allowed-outdated" });
16
+ const READ = Object.freeze({ productionMutation: false, requiresReviewedPlan: false, requiresExplicitAuthorization: false, boundaryError: null });
17
+ const PLAN = Object.freeze({ productionMutation: false, requiresReviewedPlan: false, requiresExplicitAuthorization: false, boundaryError: "E_CODE_CHANGE_REQUIRED" });
18
+ const MUTATE = Object.freeze({ productionMutation: true, requiresReviewedPlan: true, requiresExplicitAuthorization: true, boundaryError: "E_CODE_CHANGE_REQUIRED" });
19
+
20
+ function command(path, summary, inputs = [], examples = [], metadata = {}) {
21
+ const parts = path.split(".");
22
+ return Object.freeze({
23
+ path,
24
+ group: parts.length === 1 ? path : parts[0],
25
+ command: parts.join(" "),
26
+ summary,
27
+ inputs: Object.freeze(inputs),
28
+ examples: Object.freeze(examples),
29
+ ...READ,
30
+ ...LOCAL,
31
+ ...metadata
32
+ });
33
+ }
34
+
35
+ const project = option("project", "<slug|uuid>", "Writer project slug or UUID.", { required: true });
36
+ const version = option("version", "<internal-n>", "Internal workspace version number.");
37
+ const out = option("out", "<plan-file>", "Private output plan path.", { required: true });
38
+ const message = option("message", "<checkpoint-message>", "Checkpoint message.");
39
+
40
+ const COMMANDS = Object.freeze([
41
+ command("version", "Show installed CLI version and integrity.", [option("check", null, "Check npm latest.", { type: "boolean" })], ["ssobig-writer version --check"]),
42
+ command("doctor", "Run read-only installation and connectivity diagnostics.", [], ["ssobig-writer doctor"]),
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),
46
+ command("auth.logout", "Clear local credentials and cached agent state.", [], ["ssobig-writer auth logout"]),
47
+ command("agent.start", "Start the private local Writer daemon.", [], ["ssobig-writer agent start"], SERVER),
48
+ command("agent.status", "Show local daemon status.", [], ["ssobig-writer agent status"]),
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 }),
52
+ command("agent.purge", "Purge local manuscript indexes.", [], ["ssobig-writer agent purge"]),
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),
56
+ command("project.list", "List visible Writer projects.", [option("status", "<active|archived>", "Project status.")], ["ssobig-writer project list --status active"], SERVER),
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 }),
63
+ command("component.list", "List Components in a workspace.", [project, version], ["ssobig-writer component list --project <slug>"], SERVER),
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 }),
68
+ 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 }),
71
+ 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
+ command("checkpoint.status", "Show the current checkpoint head.", [project, version], ["ssobig-writer checkpoint status --project <slug>"], SERVER),
73
+ 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
+ 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 }),
77
+ 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 }),
79
+ 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
+ command("skills.status", "Inspect bundled skill installation state.", [option("target", "<workspace>", "Workspace root.", { required: true })], ["ssobig-writer skills status --target ."]),
81
+ 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"]),
82
+ command("skills.remove", "Remove only manifest-owned skill files.", [option("target", "<workspace>", "Workspace root.", { required: true }), option("managed-only", null, "Required managed-file safety acknowledgement.", { type: "boolean", required: true }), option("codex", null, "Remove only from .agents/skills.", { type: "boolean" }), option("claude", null, "Remove only from .claude/skills.", { type: "boolean" }), option("dry-run", null, "Report without changing files.", { type: "boolean" })], ["ssobig-writer skills remove --target . --managed-only --dry-run"])
83
+ ]);
84
+
85
+ const BY_PATH = new Map(COMMANDS.map(item => [item.path, item]));
86
+ if (BY_PATH.size !== COMMANDS.length) throw new Error("Writer CLI command registry contains duplicate paths.");
87
+
88
+ const GROUP_DESCRIPTIONS = Object.freeze({
89
+ version: "installation version", doctor: "installation diagnostics", "command-schema": "targeted machine metadata", auth: "staff authentication", agent: "local agent backend",
90
+ sync: "local index refresh", read: "targeted manuscript read", search: "targeted manuscript search", project: "project identity and plans",
91
+ component: "Component data and plans", asset: "asset pointers and plans", checkpoint: "workspace history", validate: "composition validation",
92
+ apply: "reviewed plan mutation", skills: "project-local agent skills"
93
+ });
94
+
95
+ function normalizeScope(value) {
96
+ if (Array.isArray(value)) return value.filter(Boolean).join(".");
97
+ return String(value || "").trim().replace(/\s+/g, ".");
98
+ }
99
+
100
+ function getCommand(value) { return BY_PATH.get(normalizeScope(value)) || null; }
101
+ function groups() { return [...new Set(COMMANDS.map(item => item.group))]; }
102
+ function groupCommands(group) { return COMMANDS.filter(item => item.group === group); }
103
+ function allowedOptions(value) {
104
+ const item = getCommand(value);
105
+ if (!item) throw cliError("E_USAGE", `지원하지 않는 Writer CLI 명령입니다: ${normalizeScope(value)}`);
106
+ return new Set([...item.inputs.filter(input => input.type !== "positional").map(input => input.name), ...GLOBAL_INPUTS.map(input => input.name)]);
107
+ }
108
+ function booleanOptions() {
109
+ return new Set([...GLOBAL_INPUTS, ...COMMANDS.flatMap(item => item.inputs)].filter(input => input.type === "boolean").map(input => input.name));
110
+ }
111
+
112
+ function publicMetadata(item, full = true) {
113
+ const base = { path: item.path, command: item.command, summary: item.summary };
114
+ if (!full) return base;
115
+ return { ...base, inputs: item.inputs, examples: item.examples, serverAccess: item.serverAccess, productionMutation: item.productionMutation, requiresReviewedPlan: item.requiresReviewedPlan, requiresExplicitAuthorization: item.requiresExplicitAuthorization, updatePolicy: item.updatePolicy, boundaryError: item.boundaryError };
116
+ }
117
+
118
+ function commandSchema(scope = "") {
119
+ const normalized = normalizeScope(scope);
120
+ if (!normalized) return { kind: "groups", groups: groups().map(group => ({ group, summary: GROUP_DESCRIPTIONS[group], commandCount: groupCommands(group).length })) };
121
+ const exact = getCommand(normalized);
122
+ if (exact) return { kind: "command", command: publicMetadata(exact, true) };
123
+ const children = groupCommands(normalized);
124
+ if (children.length) return { kind: "group", group: normalized, commands: children.map(item => publicMetadata(item, false)) };
125
+ throw cliError("E_USAGE", `알 수 없는 command-schema scope입니다: ${normalized}`);
126
+ }
127
+
128
+ function rootHelp() {
129
+ const rows = groups().map(group => ` ${group.padEnd(11)} ${GROUP_DESCRIPTIONS[group]}`);
130
+ return `SSOBIG WRITER CLI\n\nUsage:\n ssobig-writer <group|command> [options]\n ssobig-writer <group> --help\n ssobig-writer help <group> [command]\n ssobig-writer command-schema [group|group.command] [--compact]\n\nCommands:\n${rows.join("\n")}\n\nEvery command writes one JSON document. Mutations require a reviewed plan.\nUse targeted help or command-schema to discover exact inputs.`;
131
+ }
132
+
133
+ function groupHelp(group) {
134
+ const children = groupCommands(group);
135
+ if (!children.length) throw cliError("E_USAGE", `알 수 없는 help group입니다: ${group}`);
136
+ return `SSOBIG WRITER ${group}\n\nUsage:\n ssobig-writer ${group} <command> [options]\n\nCommands:\n${children.map(item => ` ${item.command.padEnd(42)} ${item.summary}`).join("\n")}\n\nUse ssobig-writer ${group} <command> --help for exact inputs.`;
137
+ }
138
+
139
+ function leafHelp(item) {
140
+ const syntax = item.inputs.map(input => input.type === "positional" ? `<${input.name}>` : `${input.required ? "" : "["}--${input.name}${input.type === "boolean" ? "" : ` ${input.value || `<${input.name}>`}`}${input.required ? "" : "]"}`).join(" ");
141
+ const inputs = item.inputs.length ? `\n\nInputs:\n${item.inputs.map(input => ` ${input.type === "positional" ? `<${input.name}>` : `--${input.name}`.padEnd(22)} ${input.required ? "required; " : ""}${input.description}`).join("\n")}` : "";
142
+ return `SSOBIG WRITER ${item.command}\n\n${item.summary}\n\nUsage:\n ssobig-writer ${item.command}${syntax ? ` ${syntax}` : ""}${inputs}\n\nExamples:\n${item.examples.map(example => ` ${example}`).join("\n")}`;
143
+ }
144
+
145
+ function helpFor(scope = "") {
146
+ const normalized = normalizeScope(scope);
147
+ if (!normalized) return rootHelp();
148
+ const exact = getCommand(normalized);
149
+ return exact ? leafHelp(exact) : groupHelp(normalized);
150
+ }
151
+
152
+ module.exports = Object.freeze({ COMMANDS, GLOBAL_INPUTS, getCommand, groups, groupCommands, allowedOptions, booleanOptions, commandSchema, helpFor, normalizeScope });