@stoneforge/quarry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +8 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/quarry-api.d.ts +268 -0
- package/dist/api/quarry-api.d.ts.map +1 -0
- package/dist/api/quarry-api.js +3905 -0
- package/dist/api/quarry-api.js.map +1 -0
- package/dist/api/types.d.ts +1359 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +204 -0
- package/dist/api/types.js.map +1 -0
- package/dist/bin/sf.d.ts +3 -0
- package/dist/bin/sf.d.ts.map +1 -0
- package/dist/bin/sf.js +9 -0
- package/dist/bin/sf.js.map +1 -0
- package/dist/cli/commands/admin.d.ts +11 -0
- package/dist/cli/commands/admin.d.ts.map +1 -0
- package/dist/cli/commands/admin.js +465 -0
- package/dist/cli/commands/admin.js.map +1 -0
- package/dist/cli/commands/alias.d.ts +8 -0
- package/dist/cli/commands/alias.d.ts.map +1 -0
- package/dist/cli/commands/alias.js +70 -0
- package/dist/cli/commands/alias.js.map +1 -0
- package/dist/cli/commands/channel.d.ts +13 -0
- package/dist/cli/commands/channel.d.ts.map +1 -0
- package/dist/cli/commands/channel.js +680 -0
- package/dist/cli/commands/channel.js.map +1 -0
- package/dist/cli/commands/completion.d.ts +8 -0
- package/dist/cli/commands/completion.d.ts.map +1 -0
- package/dist/cli/commands/completion.js +87 -0
- package/dist/cli/commands/completion.js.map +1 -0
- package/dist/cli/commands/config.d.ts +12 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +242 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/crud.d.ts +64 -0
- package/dist/cli/commands/crud.d.ts.map +1 -0
- package/dist/cli/commands/crud.js +805 -0
- package/dist/cli/commands/crud.js.map +1 -0
- package/dist/cli/commands/dep.d.ts +16 -0
- package/dist/cli/commands/dep.d.ts.map +1 -0
- package/dist/cli/commands/dep.js +499 -0
- package/dist/cli/commands/dep.js.map +1 -0
- package/dist/cli/commands/document.d.ts +12 -0
- package/dist/cli/commands/document.d.ts.map +1 -0
- package/dist/cli/commands/document.js +1039 -0
- package/dist/cli/commands/document.js.map +1 -0
- package/dist/cli/commands/embeddings.d.ts +12 -0
- package/dist/cli/commands/embeddings.d.ts.map +1 -0
- package/dist/cli/commands/embeddings.js +273 -0
- package/dist/cli/commands/embeddings.js.map +1 -0
- package/dist/cli/commands/entity.d.ts +16 -0
- package/dist/cli/commands/entity.d.ts.map +1 -0
- package/dist/cli/commands/entity.js +522 -0
- package/dist/cli/commands/entity.js.map +1 -0
- package/dist/cli/commands/gc.d.ts +10 -0
- package/dist/cli/commands/gc.d.ts.map +1 -0
- package/dist/cli/commands/gc.js +257 -0
- package/dist/cli/commands/gc.js.map +1 -0
- package/dist/cli/commands/help.d.ts +11 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +169 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/history.d.ts +9 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +160 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/identity.d.ts +18 -0
- package/dist/cli/commands/identity.d.ts.map +1 -0
- package/dist/cli/commands/identity.js +698 -0
- package/dist/cli/commands/identity.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +20 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +493 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/init.d.ts +20 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/install.d.ts +9 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +200 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/library.d.ts +12 -0
- package/dist/cli/commands/library.d.ts.map +1 -0
- package/dist/cli/commands/library.js +665 -0
- package/dist/cli/commands/library.js.map +1 -0
- package/dist/cli/commands/message.d.ts +11 -0
- package/dist/cli/commands/message.d.ts.map +1 -0
- package/dist/cli/commands/message.js +608 -0
- package/dist/cli/commands/message.js.map +1 -0
- package/dist/cli/commands/plan.d.ts +17 -0
- package/dist/cli/commands/plan.d.ts.map +1 -0
- package/dist/cli/commands/plan.js +698 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/playbook.d.ts +12 -0
- package/dist/cli/commands/playbook.d.ts.map +1 -0
- package/dist/cli/commands/playbook.js +730 -0
- package/dist/cli/commands/playbook.js.map +1 -0
- package/dist/cli/commands/reset.d.ts +12 -0
- package/dist/cli/commands/reset.d.ts.map +1 -0
- package/dist/cli/commands/reset.js +306 -0
- package/dist/cli/commands/reset.js.map +1 -0
- package/dist/cli/commands/serve.d.ts +11 -0
- package/dist/cli/commands/serve.d.ts.map +1 -0
- package/dist/cli/commands/serve.js +106 -0
- package/dist/cli/commands/serve.js.map +1 -0
- package/dist/cli/commands/stats.d.ts +8 -0
- package/dist/cli/commands/stats.d.ts.map +1 -0
- package/dist/cli/commands/stats.js +82 -0
- package/dist/cli/commands/stats.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +14 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +370 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/task.d.ts +25 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1153 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/team.d.ts +13 -0
- package/dist/cli/commands/team.d.ts.map +1 -0
- package/dist/cli/commands/team.js +471 -0
- package/dist/cli/commands/team.js.map +1 -0
- package/dist/cli/commands/workflow.d.ts +16 -0
- package/dist/cli/commands/workflow.d.ts.map +1 -0
- package/dist/cli/commands/workflow.js +753 -0
- package/dist/cli/commands/workflow.js.map +1 -0
- package/dist/cli/completion.d.ts +28 -0
- package/dist/cli/completion.d.ts.map +1 -0
- package/dist/cli/completion.js +295 -0
- package/dist/cli/completion.js.map +1 -0
- package/dist/cli/db.d.ts +38 -0
- package/dist/cli/db.d.ts.map +1 -0
- package/dist/cli/db.js +90 -0
- package/dist/cli/db.js.map +1 -0
- package/dist/cli/formatter.d.ts +87 -0
- package/dist/cli/formatter.d.ts.map +1 -0
- package/dist/cli/formatter.js +464 -0
- package/dist/cli/formatter.js.map +1 -0
- package/dist/cli/index.d.ts +33 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/parser.d.ts +45 -0
- package/dist/cli/parser.d.ts.map +1 -0
- package/dist/cli/parser.js +256 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/cli/plugin-loader.d.ts +39 -0
- package/dist/cli/plugin-loader.d.ts.map +1 -0
- package/dist/cli/plugin-loader.js +165 -0
- package/dist/cli/plugin-loader.js.map +1 -0
- package/dist/cli/plugin-registry.d.ts +50 -0
- package/dist/cli/plugin-registry.d.ts.map +1 -0
- package/dist/cli/plugin-registry.js +206 -0
- package/dist/cli/plugin-registry.js.map +1 -0
- package/dist/cli/plugin-types.d.ts +106 -0
- package/dist/cli/plugin-types.d.ts.map +1 -0
- package/dist/cli/plugin-types.js +103 -0
- package/dist/cli/plugin-types.js.map +1 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +340 -0
- package/dist/cli/runner.js.map +1 -0
- package/dist/cli/suggest.d.ts +15 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +49 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +63 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/config/config.d.ts +86 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +348 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +66 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +114 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/duration.d.ts +75 -0
- package/dist/config/duration.d.ts.map +1 -0
- package/dist/config/duration.js +190 -0
- package/dist/config/duration.js.map +1 -0
- package/dist/config/env.d.ts +67 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +207 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/file.d.ts +97 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +365 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/index.d.ts +35 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +41 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/merge.d.ts +53 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +226 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/types.d.ts +257 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +72 -0
- package/dist/config/types.js.map +1 -0
- package/dist/config/validation.d.ts +55 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +251 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/http/index.d.ts +8 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +12 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/sync-handlers.d.ts +162 -0
- package/dist/http/sync-handlers.d.ts.map +1 -0
- package/dist/http/sync-handlers.js +271 -0
- package/dist/http/sync-handlers.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +34 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +3329 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/static.d.ts +18 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +71 -0
- package/dist/server/static.js.map +1 -0
- package/dist/server/ws/broadcaster.d.ts +8 -0
- package/dist/server/ws/broadcaster.d.ts.map +1 -0
- package/dist/server/ws/broadcaster.js +7 -0
- package/dist/server/ws/broadcaster.js.map +1 -0
- package/dist/server/ws/handler.d.ts +55 -0
- package/dist/server/ws/handler.d.ts.map +1 -0
- package/dist/server/ws/handler.js +160 -0
- package/dist/server/ws/handler.js.map +1 -0
- package/dist/services/blocked-cache.d.ts +297 -0
- package/dist/services/blocked-cache.d.ts.map +1 -0
- package/dist/services/blocked-cache.js +755 -0
- package/dist/services/blocked-cache.js.map +1 -0
- package/dist/services/dependency.d.ts +205 -0
- package/dist/services/dependency.d.ts.map +1 -0
- package/dist/services/dependency.js +566 -0
- package/dist/services/dependency.js.map +1 -0
- package/dist/services/embeddings/fusion.d.ts +33 -0
- package/dist/services/embeddings/fusion.d.ts.map +1 -0
- package/dist/services/embeddings/fusion.js +34 -0
- package/dist/services/embeddings/fusion.js.map +1 -0
- package/dist/services/embeddings/index.d.ts +12 -0
- package/dist/services/embeddings/index.d.ts.map +1 -0
- package/dist/services/embeddings/index.js +10 -0
- package/dist/services/embeddings/index.js.map +1 -0
- package/dist/services/embeddings/local-provider.d.ts +31 -0
- package/dist/services/embeddings/local-provider.d.ts.map +1 -0
- package/dist/services/embeddings/local-provider.js +80 -0
- package/dist/services/embeddings/local-provider.js.map +1 -0
- package/dist/services/embeddings/service.d.ts +76 -0
- package/dist/services/embeddings/service.d.ts.map +1 -0
- package/dist/services/embeddings/service.js +153 -0
- package/dist/services/embeddings/service.js.map +1 -0
- package/dist/services/embeddings/types.d.ts +70 -0
- package/dist/services/embeddings/types.d.ts.map +1 -0
- package/dist/services/embeddings/types.js +8 -0
- package/dist/services/embeddings/types.js.map +1 -0
- package/dist/services/id-length-cache.d.ts +156 -0
- package/dist/services/id-length-cache.d.ts.map +1 -0
- package/dist/services/id-length-cache.js +197 -0
- package/dist/services/id-length-cache.js.map +1 -0
- package/dist/services/inbox.d.ts +147 -0
- package/dist/services/inbox.d.ts.map +1 -0
- package/dist/services/inbox.js +428 -0
- package/dist/services/inbox.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +10 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/priority-service.d.ts +145 -0
- package/dist/services/priority-service.d.ts.map +1 -0
- package/dist/services/priority-service.js +272 -0
- package/dist/services/priority-service.js.map +1 -0
- package/dist/services/search-utils.d.ts +47 -0
- package/dist/services/search-utils.d.ts.map +1 -0
- package/dist/services/search-utils.js +83 -0
- package/dist/services/search-utils.js.map +1 -0
- package/dist/sync/hash.d.ts +48 -0
- package/dist/sync/hash.d.ts.map +1 -0
- package/dist/sync/hash.js +136 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +16 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/merge.d.ts +80 -0
- package/dist/sync/merge.d.ts.map +1 -0
- package/dist/sync/merge.js +310 -0
- package/dist/sync/merge.js.map +1 -0
- package/dist/sync/serialization.d.ts +132 -0
- package/dist/sync/serialization.d.ts.map +1 -0
- package/dist/sync/serialization.js +306 -0
- package/dist/sync/serialization.js.map +1 -0
- package/dist/sync/service.d.ts +102 -0
- package/dist/sync/service.d.ts.map +1 -0
- package/dist/sync/service.js +493 -0
- package/dist/sync/service.js.map +1 -0
- package/dist/sync/types.d.ts +275 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +76 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/systems/identity.d.ts +479 -0
- package/dist/systems/identity.d.ts.map +1 -0
- package/dist/systems/identity.js +817 -0
- package/dist/systems/identity.js.map +1 -0
- package/dist/systems/index.d.ts +8 -0
- package/dist/systems/index.d.ts.map +1 -0
- package/dist/systems/index.js +29 -0
- package/dist/systems/index.js.map +1 -0
- package/package.json +121 -0
- package/web/assets/charts-vendor-D1YcbGux.js +55 -0
- package/web/assets/dnd-vendor-DmxE-_ZH.js +5 -0
- package/web/assets/editor-vendor-BxraAWts.js +279 -0
- package/web/assets/index-B77vv208.js +341 -0
- package/web/assets/index-CF_XnVLh.css +1 -0
- package/web/assets/router-vendor-BCKpRBrB.js +41 -0
- package/web/assets/ui-vendor-DUahGnbT.js +45 -0
- package/web/assets/utils-vendor-CfYKiENT.js +813 -0
- package/web/favicon.ico +0 -0
- package/web/index.html +23 -0
- package/web/logo.png +0 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Commands - Document management CLI interface
|
|
3
|
+
*
|
|
4
|
+
* Provides CLI commands for document operations:
|
|
5
|
+
* - doc create: Create a new document
|
|
6
|
+
* - doc list: List documents
|
|
7
|
+
* - doc history: Show document version history
|
|
8
|
+
* - doc rollback: Rollback to a previous version
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { success, failure, ExitCode } from '../types.js';
|
|
13
|
+
import { getFormatter, getOutputMode } from '../formatter.js';
|
|
14
|
+
import { createDocument, ContentType, DocumentCategory, DocumentStatus, isValidDocumentCategory, isValidDocumentStatus, } from '@stoneforge/core';
|
|
15
|
+
import { suggestCommands } from '../suggest.js';
|
|
16
|
+
import { resolveActor, createAPI } from '../db.js';
|
|
17
|
+
const docCreateOptions = [
|
|
18
|
+
{
|
|
19
|
+
name: 'title',
|
|
20
|
+
description: 'Document title',
|
|
21
|
+
hasValue: true,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'content',
|
|
25
|
+
short: 'c',
|
|
26
|
+
description: 'Document content (text)',
|
|
27
|
+
hasValue: true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'file',
|
|
31
|
+
short: 'f',
|
|
32
|
+
description: 'Read content from file',
|
|
33
|
+
hasValue: true,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'type',
|
|
37
|
+
short: 't',
|
|
38
|
+
description: 'Content type: text, markdown, json (default: text)',
|
|
39
|
+
hasValue: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'category',
|
|
43
|
+
description: 'Document category (e.g., spec, prd, reference, tutorial)',
|
|
44
|
+
hasValue: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'tag',
|
|
48
|
+
description: 'Add tag (can be repeated)',
|
|
49
|
+
hasValue: true,
|
|
50
|
+
array: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'metadata',
|
|
54
|
+
short: 'm',
|
|
55
|
+
description: "JSON metadata (e.g. '{\"key\": \"value\"}')",
|
|
56
|
+
hasValue: true,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
async function docCreateHandler(_args, options) {
|
|
60
|
+
// Must specify either --content or --file
|
|
61
|
+
if (!options.content && !options.file) {
|
|
62
|
+
return failure('Either --content or --file is required', ExitCode.INVALID_ARGUMENTS);
|
|
63
|
+
}
|
|
64
|
+
if (options.content && options.file) {
|
|
65
|
+
return failure('Cannot specify both --content and --file', ExitCode.INVALID_ARGUMENTS);
|
|
66
|
+
}
|
|
67
|
+
const { api, error } = createAPI(options, true);
|
|
68
|
+
if (error) {
|
|
69
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const actor = resolveActor(options);
|
|
73
|
+
// Get content
|
|
74
|
+
let content;
|
|
75
|
+
if (options.content) {
|
|
76
|
+
content = options.content;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const filePath = resolve(options.file);
|
|
80
|
+
if (!existsSync(filePath)) {
|
|
81
|
+
return failure(`File not found: ${filePath}`, ExitCode.NOT_FOUND);
|
|
82
|
+
}
|
|
83
|
+
content = readFileSync(filePath, 'utf-8');
|
|
84
|
+
}
|
|
85
|
+
// Parse content type
|
|
86
|
+
let contentType = ContentType.TEXT;
|
|
87
|
+
if (options.type) {
|
|
88
|
+
const validTypes = Object.values(ContentType);
|
|
89
|
+
if (!validTypes.includes(options.type)) {
|
|
90
|
+
return failure(`Invalid content type: ${options.type}. Must be one of: ${validTypes.join(', ')}`, ExitCode.VALIDATION);
|
|
91
|
+
}
|
|
92
|
+
contentType = options.type;
|
|
93
|
+
}
|
|
94
|
+
// Validate category
|
|
95
|
+
if (options.category && !isValidDocumentCategory(options.category)) {
|
|
96
|
+
const validCategories = Object.values(DocumentCategory);
|
|
97
|
+
return failure(`Invalid category: ${options.category}. Must be one of: ${validCategories.join(', ')}`, ExitCode.VALIDATION);
|
|
98
|
+
}
|
|
99
|
+
// Handle tags
|
|
100
|
+
let tags;
|
|
101
|
+
if (options.tag) {
|
|
102
|
+
tags = Array.isArray(options.tag) ? options.tag : [options.tag];
|
|
103
|
+
}
|
|
104
|
+
// Parse metadata if provided
|
|
105
|
+
let metadata;
|
|
106
|
+
if (options.metadata) {
|
|
107
|
+
try {
|
|
108
|
+
metadata = JSON.parse(options.metadata);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return failure('Invalid JSON for --metadata', ExitCode.VALIDATION);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const input = {
|
|
115
|
+
content,
|
|
116
|
+
contentType,
|
|
117
|
+
createdBy: actor,
|
|
118
|
+
...(options.title && { title: options.title }),
|
|
119
|
+
...(tags && { tags }),
|
|
120
|
+
...(metadata && { metadata }),
|
|
121
|
+
...(options.category && { category: options.category }),
|
|
122
|
+
};
|
|
123
|
+
const doc = await createDocument(input);
|
|
124
|
+
const created = await api.create(doc);
|
|
125
|
+
const mode = getOutputMode(options);
|
|
126
|
+
if (mode === 'quiet') {
|
|
127
|
+
return success(created.id);
|
|
128
|
+
}
|
|
129
|
+
return success(created, `Created document ${created.id}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
133
|
+
return failure(`Failed to create document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const docCreateCommand = {
|
|
137
|
+
name: 'create',
|
|
138
|
+
description: 'Create a new document',
|
|
139
|
+
usage: 'sf document create --content <text> | --file <path> [options]',
|
|
140
|
+
help: `Create a new document.
|
|
141
|
+
|
|
142
|
+
Options:
|
|
143
|
+
--title <title> Document title
|
|
144
|
+
-c, --content <text> Document content (inline)
|
|
145
|
+
-f, --file <path> Read content from file
|
|
146
|
+
-t, --type <type> Content type: text, markdown, json (default: text)
|
|
147
|
+
--category <category> Document category (e.g., spec, prd, reference)
|
|
148
|
+
--tag <tag> Add tag (can be repeated)
|
|
149
|
+
-m, --metadata <json> JSON metadata
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
sf document create --title "API Reference" --content "..." --category reference
|
|
153
|
+
sf document create --title "Architecture" --file spec.md --type markdown --category spec
|
|
154
|
+
sf document create --content '{"key": "value"}' --type json --tag config
|
|
155
|
+
sf document create --title "Custom Doc" --content "..." --category other --metadata '{"customCategory": "design-system"}'`,
|
|
156
|
+
options: docCreateOptions,
|
|
157
|
+
handler: docCreateHandler,
|
|
158
|
+
};
|
|
159
|
+
const docListOptions = [
|
|
160
|
+
{
|
|
161
|
+
name: 'limit',
|
|
162
|
+
short: 'l',
|
|
163
|
+
description: 'Maximum number of results',
|
|
164
|
+
hasValue: true,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'type',
|
|
168
|
+
short: 't',
|
|
169
|
+
description: 'Filter by content type (text, markdown, json)',
|
|
170
|
+
hasValue: true,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'category',
|
|
174
|
+
description: 'Filter by category (e.g., spec, prd, reference)',
|
|
175
|
+
hasValue: true,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'status',
|
|
179
|
+
description: 'Filter by status (active, archived)',
|
|
180
|
+
hasValue: true,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'all',
|
|
184
|
+
short: 'a',
|
|
185
|
+
description: 'Include archived documents',
|
|
186
|
+
hasValue: false,
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
async function docListHandler(_args, options) {
|
|
190
|
+
const { api, error } = createAPI(options);
|
|
191
|
+
if (error) {
|
|
192
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
// Build filter
|
|
196
|
+
const filter = {
|
|
197
|
+
type: 'document',
|
|
198
|
+
};
|
|
199
|
+
// Limit
|
|
200
|
+
if (options.limit) {
|
|
201
|
+
const limit = parseInt(options.limit, 10);
|
|
202
|
+
if (isNaN(limit) || limit < 1) {
|
|
203
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
204
|
+
}
|
|
205
|
+
filter.limit = limit;
|
|
206
|
+
}
|
|
207
|
+
// Category filter
|
|
208
|
+
if (options.category) {
|
|
209
|
+
if (!isValidDocumentCategory(options.category)) {
|
|
210
|
+
const validCategories = Object.values(DocumentCategory);
|
|
211
|
+
return failure(`Invalid category: ${options.category}. Must be one of: ${validCategories.join(', ')}`, ExitCode.VALIDATION);
|
|
212
|
+
}
|
|
213
|
+
filter.category = options.category;
|
|
214
|
+
}
|
|
215
|
+
// Status filter: --all includes everything, --status filters explicitly, default is active only
|
|
216
|
+
if (options.all) {
|
|
217
|
+
filter.status = [DocumentStatus.ACTIVE, DocumentStatus.ARCHIVED];
|
|
218
|
+
}
|
|
219
|
+
else if (options.status) {
|
|
220
|
+
if (!isValidDocumentStatus(options.status)) {
|
|
221
|
+
return failure(`Invalid status: ${options.status}. Must be one of: active, archived`, ExitCode.VALIDATION);
|
|
222
|
+
}
|
|
223
|
+
filter.status = options.status;
|
|
224
|
+
}
|
|
225
|
+
// Default: active only (handled by buildDocumentWhereClause)
|
|
226
|
+
const result = await api.listPaginated(filter);
|
|
227
|
+
let items = result.items;
|
|
228
|
+
// Filter by content type if specified
|
|
229
|
+
if (options.type) {
|
|
230
|
+
items = items.filter((d) => d.contentType === options.type);
|
|
231
|
+
}
|
|
232
|
+
const mode = getOutputMode(options);
|
|
233
|
+
const formatter = getFormatter(mode);
|
|
234
|
+
if (mode === 'json') {
|
|
235
|
+
return success(items);
|
|
236
|
+
}
|
|
237
|
+
if (mode === 'quiet') {
|
|
238
|
+
return success(items.map((d) => d.id).join('\n'));
|
|
239
|
+
}
|
|
240
|
+
if (items.length === 0) {
|
|
241
|
+
return success(null, 'No documents found');
|
|
242
|
+
}
|
|
243
|
+
// Build table
|
|
244
|
+
const headers = ['ID', 'TYPE', 'CATEGORY', 'STATUS', 'VERSION', 'SIZE', 'CREATED'];
|
|
245
|
+
const rows = items.map((d) => [
|
|
246
|
+
d.id,
|
|
247
|
+
d.contentType,
|
|
248
|
+
d.category ?? 'other',
|
|
249
|
+
d.status ?? 'active',
|
|
250
|
+
`v${d.version}`,
|
|
251
|
+
formatSize(d.content.length),
|
|
252
|
+
d.createdAt.split('T')[0],
|
|
253
|
+
]);
|
|
254
|
+
const table = formatter.table(headers, rows);
|
|
255
|
+
const summary = `\nShowing ${items.length} of ${result.total} documents`;
|
|
256
|
+
return success(items, table + summary);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
260
|
+
return failure(`Failed to list documents: ${message}`, ExitCode.GENERAL_ERROR);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Format size in human-readable format
|
|
265
|
+
*/
|
|
266
|
+
function formatSize(bytes) {
|
|
267
|
+
if (bytes < 1024)
|
|
268
|
+
return `${bytes}B`;
|
|
269
|
+
if (bytes < 1024 * 1024)
|
|
270
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
271
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
272
|
+
}
|
|
273
|
+
const docListCommand = {
|
|
274
|
+
name: 'list',
|
|
275
|
+
description: 'List documents',
|
|
276
|
+
usage: 'sf document list [options]',
|
|
277
|
+
help: `List documents (active only by default).
|
|
278
|
+
|
|
279
|
+
Options:
|
|
280
|
+
-l, --limit <n> Maximum results
|
|
281
|
+
-t, --type <type> Filter by content type
|
|
282
|
+
--category <category> Filter by category (e.g., spec, prd, reference)
|
|
283
|
+
--status <status> Filter by status (active, archived)
|
|
284
|
+
-a, --all Include archived documents
|
|
285
|
+
|
|
286
|
+
Examples:
|
|
287
|
+
sf document list
|
|
288
|
+
sf document list --type markdown
|
|
289
|
+
sf document list --category spec
|
|
290
|
+
sf document list --status archived
|
|
291
|
+
sf document list --all
|
|
292
|
+
sf document list --limit 10`,
|
|
293
|
+
options: docListOptions,
|
|
294
|
+
handler: docListHandler,
|
|
295
|
+
};
|
|
296
|
+
const docHistoryOptions = [
|
|
297
|
+
{
|
|
298
|
+
name: 'limit',
|
|
299
|
+
short: 'l',
|
|
300
|
+
description: 'Maximum versions to show',
|
|
301
|
+
hasValue: true,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
async function docHistoryHandler(args, options) {
|
|
305
|
+
const [docId] = args;
|
|
306
|
+
if (!docId) {
|
|
307
|
+
return failure('Usage: sf document history <document-id>', ExitCode.INVALID_ARGUMENTS);
|
|
308
|
+
}
|
|
309
|
+
const { api, error } = createAPI(options);
|
|
310
|
+
if (error) {
|
|
311
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
// Get current document to verify it exists
|
|
315
|
+
const current = await api.get(docId);
|
|
316
|
+
if (!current) {
|
|
317
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
318
|
+
}
|
|
319
|
+
if (current.type !== 'document') {
|
|
320
|
+
return failure(`Element ${docId} is not a document (type: ${current.type})`, ExitCode.VALIDATION);
|
|
321
|
+
}
|
|
322
|
+
// Check if document is deleted (tombstone)
|
|
323
|
+
const data = current;
|
|
324
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
325
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
326
|
+
}
|
|
327
|
+
// Get version history
|
|
328
|
+
const history = await api.getDocumentHistory(current.id);
|
|
329
|
+
// Apply limit
|
|
330
|
+
let versions = history;
|
|
331
|
+
if (options.limit) {
|
|
332
|
+
const limit = parseInt(options.limit, 10);
|
|
333
|
+
if (isNaN(limit) || limit < 1) {
|
|
334
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
335
|
+
}
|
|
336
|
+
versions = history.slice(0, limit);
|
|
337
|
+
}
|
|
338
|
+
const mode = getOutputMode(options);
|
|
339
|
+
const formatter = getFormatter(mode);
|
|
340
|
+
if (mode === 'json') {
|
|
341
|
+
return success(versions);
|
|
342
|
+
}
|
|
343
|
+
if (mode === 'quiet') {
|
|
344
|
+
return success(versions.map((v) => `v${v.version}`).join('\n'));
|
|
345
|
+
}
|
|
346
|
+
if (versions.length === 0) {
|
|
347
|
+
return success(null, 'No version history');
|
|
348
|
+
}
|
|
349
|
+
// Build table
|
|
350
|
+
const headers = ['VERSION', 'SIZE', 'MODIFIED', 'CURRENT'];
|
|
351
|
+
const rows = versions.map((v) => [
|
|
352
|
+
`v${v.version}`,
|
|
353
|
+
formatSize(v.content.length),
|
|
354
|
+
v.updatedAt.split('T')[0],
|
|
355
|
+
v.id === current.id ? 'Yes' : '',
|
|
356
|
+
]);
|
|
357
|
+
const table = formatter.table(headers, rows);
|
|
358
|
+
const summary = `\nDocument ${docId} has ${history.length} version(s)`;
|
|
359
|
+
return success(versions, table + summary);
|
|
360
|
+
}
|
|
361
|
+
catch (err) {
|
|
362
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
+
return failure(`Failed to get document history: ${message}`, ExitCode.GENERAL_ERROR);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const docHistoryCommand = {
|
|
367
|
+
name: 'history',
|
|
368
|
+
description: 'Show document version history',
|
|
369
|
+
usage: 'sf document history <document-id> [options]',
|
|
370
|
+
help: `Show the version history of a document.
|
|
371
|
+
|
|
372
|
+
Arguments:
|
|
373
|
+
document-id Document identifier
|
|
374
|
+
|
|
375
|
+
Options:
|
|
376
|
+
-l, --limit <n> Maximum versions to show
|
|
377
|
+
|
|
378
|
+
Examples:
|
|
379
|
+
sf document history el-doc123
|
|
380
|
+
sf document history el-doc123 --limit 5`,
|
|
381
|
+
options: docHistoryOptions,
|
|
382
|
+
handler: docHistoryHandler,
|
|
383
|
+
};
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// Document Rollback Command
|
|
386
|
+
// ============================================================================
|
|
387
|
+
async function docRollbackHandler(args, options) {
|
|
388
|
+
const [docId, versionStr] = args;
|
|
389
|
+
if (!docId || !versionStr) {
|
|
390
|
+
return failure('Usage: sf document rollback <document-id> <version>', ExitCode.INVALID_ARGUMENTS);
|
|
391
|
+
}
|
|
392
|
+
const version = parseInt(versionStr, 10);
|
|
393
|
+
if (isNaN(version) || version < 1) {
|
|
394
|
+
return failure('Version must be a positive number', ExitCode.VALIDATION);
|
|
395
|
+
}
|
|
396
|
+
const { api, error } = createAPI(options);
|
|
397
|
+
if (error) {
|
|
398
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const actor = resolveActor(options);
|
|
402
|
+
// Get the target version
|
|
403
|
+
const targetVersion = await api.getDocumentVersion(docId, version);
|
|
404
|
+
if (!targetVersion) {
|
|
405
|
+
return failure(`Version ${version} not found for document ${docId}`, ExitCode.NOT_FOUND);
|
|
406
|
+
}
|
|
407
|
+
// Get current document
|
|
408
|
+
const current = await api.get(docId);
|
|
409
|
+
if (!current) {
|
|
410
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
411
|
+
}
|
|
412
|
+
// Check if document is deleted (tombstone)
|
|
413
|
+
const data = current;
|
|
414
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
415
|
+
return failure(`Cannot rollback deleted document: ${docId}`, ExitCode.NOT_FOUND);
|
|
416
|
+
}
|
|
417
|
+
// Already at that version?
|
|
418
|
+
if (current.version === version) {
|
|
419
|
+
return success(current, `Document is already at version ${version}`);
|
|
420
|
+
}
|
|
421
|
+
// Update document with content from target version
|
|
422
|
+
// This creates a new version with the old content
|
|
423
|
+
const updated = await api.update(docId, { content: targetVersion.content }, { actor });
|
|
424
|
+
return success(updated, `Rolled back document ${docId} to version ${version} (new version: ${updated.version})`);
|
|
425
|
+
}
|
|
426
|
+
catch (err) {
|
|
427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
428
|
+
const code = err.code;
|
|
429
|
+
if (code === 'NOT_FOUND') {
|
|
430
|
+
return failure(message, ExitCode.NOT_FOUND);
|
|
431
|
+
}
|
|
432
|
+
if (code === 'INVALID_INPUT') {
|
|
433
|
+
return failure(message, ExitCode.VALIDATION);
|
|
434
|
+
}
|
|
435
|
+
return failure(`Failed to rollback document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const docRollbackCommand = {
|
|
439
|
+
name: 'rollback',
|
|
440
|
+
description: 'Rollback document to a previous version',
|
|
441
|
+
usage: 'sf document rollback <document-id> <version>',
|
|
442
|
+
help: `Rollback a document to a previous version.
|
|
443
|
+
|
|
444
|
+
This creates a new version with the content from the specified version.
|
|
445
|
+
The version history is preserved.
|
|
446
|
+
|
|
447
|
+
Arguments:
|
|
448
|
+
document-id Document identifier
|
|
449
|
+
version Version number to rollback to
|
|
450
|
+
|
|
451
|
+
Examples:
|
|
452
|
+
sf document rollback el-doc123 2`,
|
|
453
|
+
handler: docRollbackHandler,
|
|
454
|
+
};
|
|
455
|
+
const docUpdateOptions = [
|
|
456
|
+
{
|
|
457
|
+
name: 'content',
|
|
458
|
+
short: 'c',
|
|
459
|
+
description: 'New document content (text)',
|
|
460
|
+
hasValue: true,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'file',
|
|
464
|
+
short: 'f',
|
|
465
|
+
description: 'Read new content from file',
|
|
466
|
+
hasValue: true,
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'metadata',
|
|
470
|
+
short: 'm',
|
|
471
|
+
description: "JSON metadata to merge (e.g. '{\"key\": \"value\"}')",
|
|
472
|
+
hasValue: true,
|
|
473
|
+
},
|
|
474
|
+
];
|
|
475
|
+
async function docUpdateHandler(args, options) {
|
|
476
|
+
const [docId] = args;
|
|
477
|
+
if (!docId) {
|
|
478
|
+
return failure('Usage: sf document update <document-id> --content <text> | --file <path> | --metadata <json>', ExitCode.INVALID_ARGUMENTS);
|
|
479
|
+
}
|
|
480
|
+
// Must specify at least one of --content, --file, or --metadata
|
|
481
|
+
if (!options.content && !options.file && !options.metadata) {
|
|
482
|
+
return failure('At least one of --content, --file, or --metadata is required', ExitCode.INVALID_ARGUMENTS);
|
|
483
|
+
}
|
|
484
|
+
if (options.content && options.file) {
|
|
485
|
+
return failure('Cannot specify both --content and --file', ExitCode.INVALID_ARGUMENTS);
|
|
486
|
+
}
|
|
487
|
+
const { api, error } = createAPI(options);
|
|
488
|
+
if (error) {
|
|
489
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
// Verify document exists
|
|
493
|
+
const existing = await api.get(docId);
|
|
494
|
+
if (!existing) {
|
|
495
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
496
|
+
}
|
|
497
|
+
if (existing.type !== 'document') {
|
|
498
|
+
return failure(`Element ${docId} is not a document (type: ${existing.type})`, ExitCode.VALIDATION);
|
|
499
|
+
}
|
|
500
|
+
// Check if document is deleted (tombstone)
|
|
501
|
+
const data = existing;
|
|
502
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
503
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
504
|
+
}
|
|
505
|
+
const actor = resolveActor(options);
|
|
506
|
+
// Parse metadata if provided
|
|
507
|
+
let metadata;
|
|
508
|
+
if (options.metadata) {
|
|
509
|
+
try {
|
|
510
|
+
metadata = JSON.parse(options.metadata);
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
return failure('Invalid JSON for --metadata', ExitCode.VALIDATION);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Get new content if provided
|
|
517
|
+
let content;
|
|
518
|
+
if (options.content) {
|
|
519
|
+
content = options.content;
|
|
520
|
+
}
|
|
521
|
+
else if (options.file) {
|
|
522
|
+
const filePath = resolve(options.file);
|
|
523
|
+
if (!existsSync(filePath)) {
|
|
524
|
+
return failure(`File not found: ${filePath}`, ExitCode.NOT_FOUND);
|
|
525
|
+
}
|
|
526
|
+
content = readFileSync(filePath, 'utf-8');
|
|
527
|
+
}
|
|
528
|
+
// Build update payload
|
|
529
|
+
const updatePayload = {};
|
|
530
|
+
if (content !== undefined)
|
|
531
|
+
updatePayload.content = content;
|
|
532
|
+
if (metadata)
|
|
533
|
+
updatePayload.metadata = metadata;
|
|
534
|
+
// Update the document (creates a new version)
|
|
535
|
+
const updated = await api.update(docId, updatePayload, { actor });
|
|
536
|
+
const mode = getOutputMode(options);
|
|
537
|
+
if (mode === 'json') {
|
|
538
|
+
return success(updated);
|
|
539
|
+
}
|
|
540
|
+
if (mode === 'quiet') {
|
|
541
|
+
return success(updated.id);
|
|
542
|
+
}
|
|
543
|
+
return success(updated, `Updated document ${docId} (now at version ${updated.version})`);
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
547
|
+
return failure(`Failed to update document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const docUpdateCommand = {
|
|
551
|
+
name: 'update',
|
|
552
|
+
description: 'Update document content',
|
|
553
|
+
usage: 'sf document update <document-id> --content <text> | --file <path> | --metadata <json>',
|
|
554
|
+
help: `Update a document's content and/or metadata, creating a new version.
|
|
555
|
+
|
|
556
|
+
Documents are versioned - each update creates a new version while preserving
|
|
557
|
+
the history. Use 'sf document history' to view versions and 'sf document rollback' to
|
|
558
|
+
revert to a previous version.
|
|
559
|
+
|
|
560
|
+
Arguments:
|
|
561
|
+
document-id Document identifier
|
|
562
|
+
|
|
563
|
+
Options:
|
|
564
|
+
-c, --content <text> New content (inline)
|
|
565
|
+
-f, --file <path> Read new content from file
|
|
566
|
+
-m, --metadata <json> JSON metadata to merge
|
|
567
|
+
|
|
568
|
+
Examples:
|
|
569
|
+
sf document update el-doc123 --content "Updated content"
|
|
570
|
+
sf document update el-doc123 --file updated-spec.md
|
|
571
|
+
sf document update el-doc123 -c "Quick fix"
|
|
572
|
+
sf document update el-doc123 --metadata '{"purpose": "api-reference"}'
|
|
573
|
+
sf document update el-doc123 --content "New content" --metadata '{"version": 2}'`,
|
|
574
|
+
options: docUpdateOptions,
|
|
575
|
+
handler: docUpdateHandler,
|
|
576
|
+
};
|
|
577
|
+
const docShowOptions = [
|
|
578
|
+
{
|
|
579
|
+
name: 'docVersion',
|
|
580
|
+
short: 'V',
|
|
581
|
+
description: 'Show specific version',
|
|
582
|
+
hasValue: true,
|
|
583
|
+
},
|
|
584
|
+
];
|
|
585
|
+
async function docShowHandler(args, options) {
|
|
586
|
+
const [docId] = args;
|
|
587
|
+
if (!docId) {
|
|
588
|
+
return failure('Usage: sf document show <document-id> [options]', ExitCode.INVALID_ARGUMENTS);
|
|
589
|
+
}
|
|
590
|
+
const { api, error } = createAPI(options);
|
|
591
|
+
if (error) {
|
|
592
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
let doc;
|
|
596
|
+
if (options.docVersion) {
|
|
597
|
+
const version = parseInt(options.docVersion, 10);
|
|
598
|
+
if (isNaN(version) || version < 1) {
|
|
599
|
+
return failure('Version must be a positive number', ExitCode.VALIDATION);
|
|
600
|
+
}
|
|
601
|
+
doc = await api.getDocumentVersion(docId, version);
|
|
602
|
+
if (!doc) {
|
|
603
|
+
return failure(`Version ${version} not found for document ${docId}`, ExitCode.NOT_FOUND);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
doc = await api.get(docId);
|
|
608
|
+
if (!doc) {
|
|
609
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
610
|
+
}
|
|
611
|
+
if (doc.type !== 'document') {
|
|
612
|
+
return failure(`Element ${docId} is not a document (type: ${doc.type})`, ExitCode.VALIDATION);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Check if document is deleted (tombstone)
|
|
616
|
+
const data = doc;
|
|
617
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
618
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
619
|
+
}
|
|
620
|
+
const mode = getOutputMode(options);
|
|
621
|
+
const formatter = getFormatter(mode);
|
|
622
|
+
if (mode === 'json') {
|
|
623
|
+
return success(doc);
|
|
624
|
+
}
|
|
625
|
+
if (mode === 'quiet') {
|
|
626
|
+
return success(doc.content);
|
|
627
|
+
}
|
|
628
|
+
// Format document details
|
|
629
|
+
const output = formatter.element(doc);
|
|
630
|
+
return success(doc, output);
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
634
|
+
const code = err.code;
|
|
635
|
+
if (code === 'NOT_FOUND') {
|
|
636
|
+
return failure(message, ExitCode.NOT_FOUND);
|
|
637
|
+
}
|
|
638
|
+
if (code === 'INVALID_INPUT') {
|
|
639
|
+
return failure(message, ExitCode.VALIDATION);
|
|
640
|
+
}
|
|
641
|
+
return failure(`Failed to show document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const docShowCommand = {
|
|
645
|
+
name: 'show',
|
|
646
|
+
description: 'Show document details',
|
|
647
|
+
usage: 'sf document show <document-id> [options]',
|
|
648
|
+
help: `Show document details and content.
|
|
649
|
+
|
|
650
|
+
Arguments:
|
|
651
|
+
document-id Document identifier
|
|
652
|
+
|
|
653
|
+
Options:
|
|
654
|
+
-V, --docVersion <n> Show specific version
|
|
655
|
+
|
|
656
|
+
Examples:
|
|
657
|
+
sf document show el-doc123
|
|
658
|
+
sf document show el-doc123 --docVersion 2
|
|
659
|
+
sf document show el-doc123 --quiet # Output content only`,
|
|
660
|
+
options: docShowOptions,
|
|
661
|
+
handler: docShowHandler,
|
|
662
|
+
};
|
|
663
|
+
// ============================================================================
|
|
664
|
+
// Document Archive Command
|
|
665
|
+
// ============================================================================
|
|
666
|
+
async function docArchiveHandler(args, options) {
|
|
667
|
+
const [docId] = args;
|
|
668
|
+
if (!docId) {
|
|
669
|
+
return failure('Usage: sf document archive <document-id>', ExitCode.INVALID_ARGUMENTS);
|
|
670
|
+
}
|
|
671
|
+
const { api, error } = createAPI(options);
|
|
672
|
+
if (error) {
|
|
673
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const existing = await api.get(docId);
|
|
677
|
+
if (!existing) {
|
|
678
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
679
|
+
}
|
|
680
|
+
if (existing.type !== 'document') {
|
|
681
|
+
return failure(`Element ${docId} is not a document`, ExitCode.VALIDATION);
|
|
682
|
+
}
|
|
683
|
+
const updated = await api.update(docId, { status: DocumentStatus.ARCHIVED }, { actor: resolveActor(options) });
|
|
684
|
+
return success(updated, `Archived document ${docId}`);
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
688
|
+
return failure(`Failed to archive document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const docArchiveCommand = {
|
|
692
|
+
name: 'archive',
|
|
693
|
+
description: 'Archive a document',
|
|
694
|
+
usage: 'sf document archive <document-id>',
|
|
695
|
+
help: `Archive a document. Archived documents are hidden from default list/search.
|
|
696
|
+
|
|
697
|
+
Arguments:
|
|
698
|
+
document-id Document identifier
|
|
699
|
+
|
|
700
|
+
Examples:
|
|
701
|
+
sf document archive el-doc123`,
|
|
702
|
+
handler: docArchiveHandler,
|
|
703
|
+
};
|
|
704
|
+
const docDeleteOptions = [
|
|
705
|
+
{
|
|
706
|
+
name: 'reason',
|
|
707
|
+
short: 'r',
|
|
708
|
+
description: 'Reason for deletion',
|
|
709
|
+
hasValue: true,
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
name: 'force',
|
|
713
|
+
short: 'f',
|
|
714
|
+
description: 'Skip confirmation',
|
|
715
|
+
hasValue: false,
|
|
716
|
+
},
|
|
717
|
+
];
|
|
718
|
+
async function docDeleteHandler(args, options) {
|
|
719
|
+
const [docId] = args;
|
|
720
|
+
if (!docId) {
|
|
721
|
+
return failure('Usage: sf document delete <document-id>', ExitCode.INVALID_ARGUMENTS);
|
|
722
|
+
}
|
|
723
|
+
const { api, error } = createAPI(options);
|
|
724
|
+
if (error) {
|
|
725
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const existing = await api.get(docId);
|
|
729
|
+
if (!existing) {
|
|
730
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
731
|
+
}
|
|
732
|
+
if (existing.type !== 'document') {
|
|
733
|
+
return failure(`Element ${docId} is not a document`, ExitCode.VALIDATION);
|
|
734
|
+
}
|
|
735
|
+
// Check if document is already deleted (tombstone)
|
|
736
|
+
const data = existing;
|
|
737
|
+
if (data.status === 'tombstone' || data.deletedAt) {
|
|
738
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
739
|
+
}
|
|
740
|
+
const actor = resolveActor(options);
|
|
741
|
+
await api.delete(docId, { actor, reason: options.reason });
|
|
742
|
+
const mode = getOutputMode(options);
|
|
743
|
+
if (mode === 'json') {
|
|
744
|
+
return success({ id: docId, deleted: true, type: 'document' });
|
|
745
|
+
}
|
|
746
|
+
if (mode === 'quiet') {
|
|
747
|
+
return success(docId);
|
|
748
|
+
}
|
|
749
|
+
return success(null, `Deleted document ${docId}`);
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
753
|
+
const code = err.code;
|
|
754
|
+
if (code === 'NOT_FOUND') {
|
|
755
|
+
return failure(message, ExitCode.NOT_FOUND);
|
|
756
|
+
}
|
|
757
|
+
return failure(`Failed to delete document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const docDeleteCommand = {
|
|
761
|
+
name: 'delete',
|
|
762
|
+
description: 'Delete a document (soft-delete)',
|
|
763
|
+
usage: 'sf document delete <document-id> [options]',
|
|
764
|
+
help: `Delete a document (soft-delete via tombstone).
|
|
765
|
+
|
|
766
|
+
Arguments:
|
|
767
|
+
document-id Document identifier
|
|
768
|
+
|
|
769
|
+
Options:
|
|
770
|
+
-r, --reason <text> Reason for deletion
|
|
771
|
+
-f, --force Skip confirmation
|
|
772
|
+
|
|
773
|
+
Examples:
|
|
774
|
+
sf document delete el-doc123
|
|
775
|
+
sf document delete el-doc123 --reason "outdated"
|
|
776
|
+
sf document delete el-doc123 --force`,
|
|
777
|
+
options: docDeleteOptions,
|
|
778
|
+
handler: docDeleteHandler,
|
|
779
|
+
};
|
|
780
|
+
// ============================================================================
|
|
781
|
+
// Document Unarchive Command
|
|
782
|
+
// ============================================================================
|
|
783
|
+
async function docUnarchiveHandler(args, options) {
|
|
784
|
+
const [docId] = args;
|
|
785
|
+
if (!docId) {
|
|
786
|
+
return failure('Usage: sf document unarchive <document-id>', ExitCode.INVALID_ARGUMENTS);
|
|
787
|
+
}
|
|
788
|
+
const { api, error } = createAPI(options);
|
|
789
|
+
if (error) {
|
|
790
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const existing = await api.get(docId);
|
|
794
|
+
if (!existing) {
|
|
795
|
+
return failure(`Document not found: ${docId}`, ExitCode.NOT_FOUND);
|
|
796
|
+
}
|
|
797
|
+
if (existing.type !== 'document') {
|
|
798
|
+
return failure(`Element ${docId} is not a document`, ExitCode.VALIDATION);
|
|
799
|
+
}
|
|
800
|
+
const updated = await api.update(docId, { status: DocumentStatus.ACTIVE }, { actor: resolveActor(options) });
|
|
801
|
+
return success(updated, `Unarchived document ${docId}`);
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
805
|
+
return failure(`Failed to unarchive document: ${message}`, ExitCode.GENERAL_ERROR);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const docUnarchiveCommand = {
|
|
809
|
+
name: 'unarchive',
|
|
810
|
+
description: 'Unarchive a document',
|
|
811
|
+
usage: 'sf document unarchive <document-id>',
|
|
812
|
+
help: `Unarchive a document, making it visible in default list/search again.
|
|
813
|
+
|
|
814
|
+
Arguments:
|
|
815
|
+
document-id Document identifier
|
|
816
|
+
|
|
817
|
+
Examples:
|
|
818
|
+
sf document unarchive el-doc123`,
|
|
819
|
+
handler: docUnarchiveHandler,
|
|
820
|
+
};
|
|
821
|
+
const docSearchOptions = [
|
|
822
|
+
{
|
|
823
|
+
name: 'category',
|
|
824
|
+
description: 'Filter by category',
|
|
825
|
+
hasValue: true,
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
name: 'status',
|
|
829
|
+
description: 'Filter by status',
|
|
830
|
+
hasValue: true,
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: 'limit',
|
|
834
|
+
short: 'l',
|
|
835
|
+
description: 'Maximum results',
|
|
836
|
+
hasValue: true,
|
|
837
|
+
},
|
|
838
|
+
];
|
|
839
|
+
async function docSearchHandler(args, options) {
|
|
840
|
+
if (args.length === 0) {
|
|
841
|
+
return failure('Search query is required. Usage: sf document search <query>', ExitCode.INVALID_ARGUMENTS);
|
|
842
|
+
}
|
|
843
|
+
const { api, error } = createAPI(options);
|
|
844
|
+
if (error) {
|
|
845
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const query = args.join(' ');
|
|
849
|
+
// Validate category
|
|
850
|
+
if (options.category) {
|
|
851
|
+
if (!isValidDocumentCategory(options.category)) {
|
|
852
|
+
const validCategories = Object.values(DocumentCategory);
|
|
853
|
+
return failure(`Invalid category: ${options.category}. Must be one of: ${validCategories.join(', ')}`, ExitCode.VALIDATION);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Validate status
|
|
857
|
+
if (options.status) {
|
|
858
|
+
if (!isValidDocumentStatus(options.status)) {
|
|
859
|
+
return failure(`Invalid status: ${options.status}. Must be one of: active, archived`, ExitCode.VALIDATION);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
// Validate limit
|
|
863
|
+
let hardCap = 50;
|
|
864
|
+
if (options.limit) {
|
|
865
|
+
hardCap = parseInt(options.limit, 10);
|
|
866
|
+
if (isNaN(hardCap) || hardCap < 1) {
|
|
867
|
+
return failure('Limit must be a positive number', ExitCode.VALIDATION);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const searchOptions = { hardCap };
|
|
871
|
+
if (options.category)
|
|
872
|
+
searchOptions.category = options.category;
|
|
873
|
+
if (options.status)
|
|
874
|
+
searchOptions.status = options.status;
|
|
875
|
+
const results = await api.searchDocumentsFTS(query, searchOptions);
|
|
876
|
+
const mode = getOutputMode(options);
|
|
877
|
+
const formatter = getFormatter(mode);
|
|
878
|
+
if (mode === 'json') {
|
|
879
|
+
return success(results);
|
|
880
|
+
}
|
|
881
|
+
if (mode === 'quiet') {
|
|
882
|
+
return success(results.map((r) => r.document.id).join('\n'));
|
|
883
|
+
}
|
|
884
|
+
if (results.length === 0) {
|
|
885
|
+
return success(null, 'No documents found');
|
|
886
|
+
}
|
|
887
|
+
const headers = ['ID', 'SCORE', 'TITLE', 'CATEGORY', 'SNIPPET'];
|
|
888
|
+
const rows = results.map((r) => [
|
|
889
|
+
r.document.id,
|
|
890
|
+
r.score.toFixed(2),
|
|
891
|
+
(r.document.title ?? '').slice(0, 40),
|
|
892
|
+
r.document.category ?? 'other',
|
|
893
|
+
r.snippet.slice(0, 60).replace(/\n/g, ' '),
|
|
894
|
+
]);
|
|
895
|
+
const table = formatter.table(headers, rows);
|
|
896
|
+
const summary = `\n${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`;
|
|
897
|
+
return success(results, table + summary);
|
|
898
|
+
}
|
|
899
|
+
catch (err) {
|
|
900
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
901
|
+
return failure(`Search failed: ${message}`, ExitCode.GENERAL_ERROR);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
const docSearchCommand = {
|
|
905
|
+
name: 'search',
|
|
906
|
+
description: 'Full-text search documents',
|
|
907
|
+
usage: 'sf document search <query> [options]',
|
|
908
|
+
help: `Full-text search documents using FTS5 with BM25 ranking.
|
|
909
|
+
|
|
910
|
+
Arguments:
|
|
911
|
+
query Search query text
|
|
912
|
+
|
|
913
|
+
Options:
|
|
914
|
+
-l, --limit <n> Maximum results (default: 50)
|
|
915
|
+
--category <category> Filter by category (e.g., spec, prd, reference)
|
|
916
|
+
--status <status> Filter by status (active, archived)
|
|
917
|
+
|
|
918
|
+
Examples:
|
|
919
|
+
sf document search "API authentication"
|
|
920
|
+
sf document search "migration" --category spec
|
|
921
|
+
sf document search "config" --limit 5`,
|
|
922
|
+
options: docSearchOptions,
|
|
923
|
+
handler: docSearchHandler,
|
|
924
|
+
};
|
|
925
|
+
// ============================================================================
|
|
926
|
+
// Document Reindex Command
|
|
927
|
+
// ============================================================================
|
|
928
|
+
async function docReindexHandler(_args, options) {
|
|
929
|
+
const { api, error } = createAPI(options);
|
|
930
|
+
if (error) {
|
|
931
|
+
return failure(error, ExitCode.GENERAL_ERROR);
|
|
932
|
+
}
|
|
933
|
+
try {
|
|
934
|
+
const result = api.reindexAllDocumentsFTS();
|
|
935
|
+
const mode = getOutputMode(options);
|
|
936
|
+
if (mode === 'json') {
|
|
937
|
+
return success(result);
|
|
938
|
+
}
|
|
939
|
+
return success(null, `Reindexed ${result.indexed} documents for FTS search${result.errors > 0 ? ` (${result.errors} errors)` : ''}`);
|
|
940
|
+
}
|
|
941
|
+
catch (err) {
|
|
942
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
943
|
+
return failure(`Failed to reindex documents: ${message}`, ExitCode.GENERAL_ERROR);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const docReindexCommand = {
|
|
947
|
+
name: 'reindex',
|
|
948
|
+
description: 'Rebuild full-text search index',
|
|
949
|
+
usage: 'sf document reindex',
|
|
950
|
+
help: `Rebuild the FTS5 full-text search index for all documents.
|
|
951
|
+
|
|
952
|
+
Iterates all documents (including archived) and re-indexes them
|
|
953
|
+
in the FTS5 virtual table. Use this after migration or if search
|
|
954
|
+
results seem stale.
|
|
955
|
+
|
|
956
|
+
Examples:
|
|
957
|
+
sf document reindex`,
|
|
958
|
+
handler: docReindexHandler,
|
|
959
|
+
};
|
|
960
|
+
// ============================================================================
|
|
961
|
+
// Document Root Command
|
|
962
|
+
// ============================================================================
|
|
963
|
+
export const documentCommand = {
|
|
964
|
+
name: 'document',
|
|
965
|
+
description: 'Manage documents (versioned content)',
|
|
966
|
+
usage: 'sf document <subcommand> [options]',
|
|
967
|
+
help: `Manage documents - versioned content storage.
|
|
968
|
+
|
|
969
|
+
Documents store content with automatic versioning. Each update creates
|
|
970
|
+
a new version, and you can view history or rollback to any previous version.
|
|
971
|
+
|
|
972
|
+
Subcommands:
|
|
973
|
+
create Create a new document
|
|
974
|
+
list List documents (active only by default)
|
|
975
|
+
search Full-text search documents
|
|
976
|
+
show Show document details
|
|
977
|
+
update Update document content (creates new version)
|
|
978
|
+
history Show version history
|
|
979
|
+
rollback Rollback to a previous version
|
|
980
|
+
archive Archive a document
|
|
981
|
+
unarchive Unarchive a document
|
|
982
|
+
delete Delete a document (soft-delete)
|
|
983
|
+
reindex Rebuild full-text search index
|
|
984
|
+
|
|
985
|
+
Examples:
|
|
986
|
+
sf document create --content "Hello world"
|
|
987
|
+
sf document create --file notes.md --type markdown --category spec
|
|
988
|
+
sf document list
|
|
989
|
+
sf document list --category reference --all
|
|
990
|
+
sf document search "API authentication"
|
|
991
|
+
sf document search "migration" --category spec
|
|
992
|
+
sf document show el-doc123
|
|
993
|
+
sf document update el-doc123 --content "New content"
|
|
994
|
+
sf document update el-doc123 --file updated.md
|
|
995
|
+
sf document history el-doc123
|
|
996
|
+
sf document rollback el-doc123 2
|
|
997
|
+
sf document archive el-doc123
|
|
998
|
+
sf document unarchive el-doc123
|
|
999
|
+
sf document delete el-doc123
|
|
1000
|
+
sf document reindex`,
|
|
1001
|
+
subcommands: {
|
|
1002
|
+
create: docCreateCommand,
|
|
1003
|
+
list: docListCommand,
|
|
1004
|
+
search: docSearchCommand,
|
|
1005
|
+
show: docShowCommand,
|
|
1006
|
+
update: docUpdateCommand,
|
|
1007
|
+
history: docHistoryCommand,
|
|
1008
|
+
rollback: docRollbackCommand,
|
|
1009
|
+
archive: docArchiveCommand,
|
|
1010
|
+
unarchive: docUnarchiveCommand,
|
|
1011
|
+
delete: docDeleteCommand,
|
|
1012
|
+
reindex: docReindexCommand,
|
|
1013
|
+
// Aliases (hidden from --help via dedup in getCommandHelp)
|
|
1014
|
+
new: docCreateCommand,
|
|
1015
|
+
add: docCreateCommand,
|
|
1016
|
+
ls: docListCommand,
|
|
1017
|
+
rm: docDeleteCommand,
|
|
1018
|
+
get: docShowCommand,
|
|
1019
|
+
view: docShowCommand,
|
|
1020
|
+
edit: docUpdateCommand,
|
|
1021
|
+
find: docSearchCommand,
|
|
1022
|
+
},
|
|
1023
|
+
handler: async (args, options) => {
|
|
1024
|
+
// Default to list if no subcommand
|
|
1025
|
+
if (args.length === 0) {
|
|
1026
|
+
return docListHandler(args, options);
|
|
1027
|
+
}
|
|
1028
|
+
// Show "did you mean?" for unknown subcommands
|
|
1029
|
+
const subNames = Object.keys(documentCommand.subcommands);
|
|
1030
|
+
const suggestions = suggestCommands(args[0], subNames);
|
|
1031
|
+
let msg = `Unknown subcommand: ${args[0]}`;
|
|
1032
|
+
if (suggestions.length > 0) {
|
|
1033
|
+
msg += `\n\nDid you mean?\n${suggestions.map(s => ` ${s}`).join('\n')}`;
|
|
1034
|
+
}
|
|
1035
|
+
msg += '\n\nRun "sf document --help" to see available subcommands.';
|
|
1036
|
+
return failure(msg, ExitCode.INVALID_ARGUMENTS);
|
|
1037
|
+
},
|
|
1038
|
+
};
|
|
1039
|
+
//# sourceMappingURL=document.js.map
|