intentdocs 0.8.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/README.md +261 -0
- package/bin/intent.js +2 -0
- package/dist/chunk-4OWPN2RE.js +249 -0
- package/dist/chunk-6FGYJ3W2.js +140 -0
- package/dist/chunk-6XKB6RTU.js +123 -0
- package/dist/chunk-ICOMMHKG.js +48 -0
- package/dist/chunk-IKF3ZLHK.js +621 -0
- package/dist/chunk-ITQUQYWB.js +21 -0
- package/dist/chunk-PCNDP5SX.js +34 -0
- package/dist/chunk-WPVDURTJ.js +79 -0
- package/dist/complexity-NSSYVT54.js +79 -0
- package/dist/import-prd-CKV3R67G.js +134 -0
- package/dist/index.js +2821 -0
- package/dist/keys-EDWET3WG.js +90 -0
- package/dist/log-TT2UCJP7.js +94 -0
- package/dist/map-extract-I3AACTGL.js +98 -0
- package/dist/map-import-IYUMRASO.js +358 -0
- package/dist/map-schema-JVL3O2TJ.js +114 -0
- package/dist/prompt-WAWCRGHN.js +16 -0
- package/dist/research-6SXJ37TI.js +59 -0
- package/dist/scope-OGKQ472D.js +10 -0
- package/dist/setup-SOI2HQAL.js +11 -0
- package/package.json +43 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ask,
|
|
3
|
+
closePrompt
|
|
4
|
+
} from "./chunk-WPVDURTJ.js";
|
|
5
|
+
import {
|
|
6
|
+
requireAuth
|
|
7
|
+
} from "./chunk-PCNDP5SX.js";
|
|
8
|
+
|
|
9
|
+
// src/commands/keys.ts
|
|
10
|
+
var KEYS_DOC_URL = "https://console.anthropic.com/settings/keys";
|
|
11
|
+
async function byokRequest(method, body) {
|
|
12
|
+
const auth = requireAuth();
|
|
13
|
+
const res = await fetch(`${auth.apiBase}/api/settings/byok`, {
|
|
14
|
+
method,
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${auth.token}`,
|
|
17
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
18
|
+
},
|
|
19
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
20
|
+
});
|
|
21
|
+
const data = await res.json().catch(() => null);
|
|
22
|
+
return { ok: res.ok, status: res.status, data };
|
|
23
|
+
}
|
|
24
|
+
async function keysSet(key) {
|
|
25
|
+
let value = key?.trim();
|
|
26
|
+
if (!value) {
|
|
27
|
+
console.log("\n Optionally add your Anthropic API key to unlock AI features on any plan.");
|
|
28
|
+
console.log(" Your key is encrypted at rest and only used for your AI operations.");
|
|
29
|
+
console.log(` Get one at: ${KEYS_DOC_URL}
|
|
30
|
+
`);
|
|
31
|
+
value = (await ask(" Anthropic API key (sk-ant-...): ")).trim();
|
|
32
|
+
closePrompt();
|
|
33
|
+
}
|
|
34
|
+
if (!value) {
|
|
35
|
+
console.error(" No key provided. Run `intent keys set` to try again.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const { ok, data } = await byokRequest("POST", { key: value });
|
|
40
|
+
if (!ok) {
|
|
41
|
+
console.error(`
|
|
42
|
+
\u2717 ${data?.message ?? "Failed to save your key. Try again."}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
console.log(`
|
|
46
|
+
\u2705 Key saved: ${data?.maskedKey}`);
|
|
47
|
+
console.log(" AI commands (complexity, research, import) now run on your key.\n");
|
|
48
|
+
} catch {
|
|
49
|
+
console.error("\n \u2717 Could not reach intentdocs. Check your connection and try again.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function keysStatus() {
|
|
54
|
+
try {
|
|
55
|
+
const { ok, data } = await byokRequest("GET");
|
|
56
|
+
if (!ok) {
|
|
57
|
+
console.error(" \u2717 Failed to fetch key status.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
if (data?.enabled) {
|
|
61
|
+
console.log(`
|
|
62
|
+
Anthropic API key: ${data.maskedKey ?? "(stored but unreadable \u2014 re-run `intent keys set`)"}`);
|
|
63
|
+
console.log(" AI features run on your key.\n");
|
|
64
|
+
} else {
|
|
65
|
+
console.log("\n No Anthropic API key set.");
|
|
66
|
+
console.log(" Add one with `intent keys set` to unlock AI features on any plan.\n");
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
console.error(" \u2717 Could not reach intentdocs. Check your connection and try again.");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function keysRemove() {
|
|
74
|
+
try {
|
|
75
|
+
const { ok } = await byokRequest("DELETE");
|
|
76
|
+
if (!ok) {
|
|
77
|
+
console.error(" \u2717 Failed to remove your key.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
console.log("\n Key removed. AI features fall back to your plan's platform limits.\n");
|
|
81
|
+
} catch {
|
|
82
|
+
console.error(" \u2717 Could not reach intentdocs. Check your connection and try again.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
keysRemove,
|
|
88
|
+
keysSet,
|
|
89
|
+
keysStatus
|
|
90
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendNoteToContext,
|
|
3
|
+
storyContextPath
|
|
4
|
+
} from "./chunk-6FGYJ3W2.js";
|
|
5
|
+
import {
|
|
6
|
+
ask,
|
|
7
|
+
closePrompt
|
|
8
|
+
} from "./chunk-WPVDURTJ.js";
|
|
9
|
+
import {
|
|
10
|
+
apiFetch
|
|
11
|
+
} from "./chunk-6XKB6RTU.js";
|
|
12
|
+
import {
|
|
13
|
+
requireConfig
|
|
14
|
+
} from "./chunk-ICOMMHKG.js";
|
|
15
|
+
import "./chunk-PCNDP5SX.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/log.ts
|
|
18
|
+
async function logDecision(title, opts) {
|
|
19
|
+
const config = requireConfig();
|
|
20
|
+
if (!title) {
|
|
21
|
+
title = await ask(" Decision title: ");
|
|
22
|
+
}
|
|
23
|
+
let description = opts?.description;
|
|
24
|
+
if (!description) {
|
|
25
|
+
description = await ask(" Description (what was decided and why): ");
|
|
26
|
+
}
|
|
27
|
+
if (!title?.trim() || !description?.trim()) {
|
|
28
|
+
console.log(" Title and description required.");
|
|
29
|
+
closePrompt();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const res = await apiFetch(config, "/decisions", {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
title: title.trim(),
|
|
37
|
+
description: description.trim(),
|
|
38
|
+
category: opts?.category ?? "technical",
|
|
39
|
+
story_id: opts?.story ?? void 0
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
if (res.ok) {
|
|
43
|
+
console.log(` \u2713 Decision logged: ${title}`);
|
|
44
|
+
} else {
|
|
45
|
+
console.error(" Failed to log decision.");
|
|
46
|
+
}
|
|
47
|
+
closePrompt();
|
|
48
|
+
}
|
|
49
|
+
async function logNote(storyId, note) {
|
|
50
|
+
const config = requireConfig();
|
|
51
|
+
if (!note) {
|
|
52
|
+
note = await ask(" Note: ");
|
|
53
|
+
}
|
|
54
|
+
if (!note?.trim()) {
|
|
55
|
+
console.log(" Note required.");
|
|
56
|
+
closePrompt();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const listRes = await apiFetch(config, "/stories");
|
|
60
|
+
if (!listRes.ok) {
|
|
61
|
+
console.error(" Failed to fetch stories.");
|
|
62
|
+
closePrompt();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const data = await listRes.json();
|
|
66
|
+
const match = data.stories.find((s) => s.id.startsWith(storyId));
|
|
67
|
+
if (!match) {
|
|
68
|
+
console.error(` No story found matching "${storyId}"`);
|
|
69
|
+
closePrompt();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const res = await apiFetch(config, "/stories", {
|
|
73
|
+
method: "PATCH",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({
|
|
76
|
+
stories: [{ id: match.id, notes: note.trim() }]
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
if (res.ok) {
|
|
80
|
+
console.log(` \u2713 Note added to: ${match.title.slice(0, 60)}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.error(" Failed to add note.");
|
|
83
|
+
}
|
|
84
|
+
const appended = appendNoteToContext(match.id, note.trim());
|
|
85
|
+
if (appended) {
|
|
86
|
+
const relPath = storyContextPath(match.id).replace(process.cwd() + "/", "");
|
|
87
|
+
console.log(` \u2713 Updated local context: ${relPath}`);
|
|
88
|
+
}
|
|
89
|
+
closePrompt();
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
logDecision,
|
|
93
|
+
logNote
|
|
94
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
requireConfig
|
|
3
|
+
} from "./chunk-ICOMMHKG.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/map-extract.ts
|
|
6
|
+
function mapExtract() {
|
|
7
|
+
requireConfig();
|
|
8
|
+
const prompt = `You are reverse-engineering a *user-facing story map* from an existing codebase for intentdocs. Your job is to describe _what real customers do in this product_ \u2014 not to document the code. Work only from evidence in the repository. Do not invent product intent.
|
|
9
|
+
|
|
10
|
+
1. Investigate (read-only)
|
|
11
|
+
Build an understanding of the product from these sources, in roughly this priority:
|
|
12
|
+
\u2022 User entry points \u2014 routes/pages (e.g. app/, pages/, router configs), and the navigation between them. These are the strongest signal for what users _do_.
|
|
13
|
+
\u2022 Auth & access boundaries \u2014 what's public vs. authenticated vs. role-gated. This tells you who each activity is _for_.
|
|
14
|
+
\u2022 API surface customers hit \u2014 request handlers, server actions, GraphQL/REST endpoints that are triggered by user actions (not internal cron/admin-only jobs).
|
|
15
|
+
\u2022 Data model \u2014 DB schema, migrations, ORM models. Extract the core entities and their relationships.
|
|
16
|
+
\u2022 Stack \u2014 read package.json / lockfiles / framework config to identify the actual frameworks, auth, DB, and payment providers in use.
|
|
17
|
+
\u2022 Billing / plans \u2014 if there's a payments integration, capture the plans and what each gates.
|
|
18
|
+
|
|
19
|
+
2. Rules of evidence
|
|
20
|
+
\u2022 Map only user-facing behavior. Exclude dead code, half-built features behind disabled flags, internal tooling, admin-only scripts, and pure infrastructure.
|
|
21
|
+
\u2022 If a feature exists in code but you can't tell whether it's live or what user need it serves, do not guess \u2014 add it to open_questions instead.
|
|
22
|
+
\u2022 Prefer describing activities in the user's language ("Invite a teammate"), not the code's ("POST /api/memberships").
|
|
23
|
+
\u2022 When features appear behind a paywall/plan gate, note which plan unlocks them.
|
|
24
|
+
|
|
25
|
+
3. Output \u2014 write storymap.json to the repo root
|
|
26
|
+
Emit only valid JSON in exactly this shape. No prose, no markdown fences in the file.
|
|
27
|
+
|
|
28
|
+
{
|
|
29
|
+
"schema_version": "1",
|
|
30
|
+
"source": "brownfield-extraction",
|
|
31
|
+
"product_summary": "1-2 sentences: what this product does and for whom.",
|
|
32
|
+
"backbone": [
|
|
33
|
+
{
|
|
34
|
+
"activity": "High-level thing a user does (backbone step)",
|
|
35
|
+
"tasks": [
|
|
36
|
+
{
|
|
37
|
+
"task": "A concrete task under this activity",
|
|
38
|
+
"stories": [
|
|
39
|
+
{
|
|
40
|
+
"story": "As a <role>, I can <do something> so that <outcome>",
|
|
41
|
+
"evidence": "file path(s) or route that prove this exists",
|
|
42
|
+
"plan_gate": "free | pro | team | null",
|
|
43
|
+
"confidence": "high | medium | low"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"data_model": [
|
|
51
|
+
{
|
|
52
|
+
"entity": "EntityName",
|
|
53
|
+
"description": "what it represents to the user",
|
|
54
|
+
"key_fields": ["field_a", "field_b"],
|
|
55
|
+
"relationships": ["belongs_to X", "has_many Y"],
|
|
56
|
+
"evidence": "migration or model file path"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"build_order": [
|
|
60
|
+
"Ordered list of the major capabilities as they appear to have been built / layered"
|
|
61
|
+
],
|
|
62
|
+
"critical_assumptions": [
|
|
63
|
+
"Assumptions this product's design bakes in (e.g. 'users have a GitHub repo')"
|
|
64
|
+
],
|
|
65
|
+
"open_questions": [
|
|
66
|
+
"Things you could NOT determine from code and need the founder to confirm"
|
|
67
|
+
],
|
|
68
|
+
"stack": {
|
|
69
|
+
"framework": "",
|
|
70
|
+
"auth": "",
|
|
71
|
+
"database": "",
|
|
72
|
+
"payments": "",
|
|
73
|
+
"ai": ""
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Field rules:
|
|
78
|
+
\u2022 Every story MUST start with "As a " or "As an " followed by a role. Example: "As a founder, I can invite teammates so that we can collaborate". The importer will reject stories that don't match this format.
|
|
79
|
+
\u2022 Activities have only two fields: "activity" and "tasks". Do NOT add "user_role" or any other field to activities.
|
|
80
|
+
\u2022 Every story MUST include an evidence path. If you have no evidence, it doesn't belong in the backbone \u2014 move it to open_questions.
|
|
81
|
+
\u2022 confidence: "low" is expected and useful. Be honest; a low-confidence story the founder can correct is worth more than a confident wrong one.
|
|
82
|
+
\u2022 Keep the backbone to the ~5-9 activities that define the product. Don't flatten every endpoint into its own activity.
|
|
83
|
+
|
|
84
|
+
4. Hand back to intentdocs
|
|
85
|
+
After writing storymap.json, run:
|
|
86
|
+
|
|
87
|
+
intent map import ./storymap.json
|
|
88
|
+
|
|
89
|
+
Then tell me (the founder): a one-paragraph summary of the map you produced, the count of stories by confidence level, and the top open questions you need me to answer.`;
|
|
90
|
+
console.log("\n Copy the prompt below and paste it into your coding agent");
|
|
91
|
+
console.log(" (Claude Code, Cursor, etc.) from the root of your repo.\n");
|
|
92
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
93
|
+
console.log(prompt);
|
|
94
|
+
console.log("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
mapExtract
|
|
98
|
+
};
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closePrompt,
|
|
3
|
+
confirm
|
|
4
|
+
} from "./chunk-WPVDURTJ.js";
|
|
5
|
+
import {
|
|
6
|
+
isPlanGated,
|
|
7
|
+
printUpgradeMessage
|
|
8
|
+
} from "./chunk-ITQUQYWB.js";
|
|
9
|
+
import {
|
|
10
|
+
apiUrl
|
|
11
|
+
} from "./chunk-6XKB6RTU.js";
|
|
12
|
+
import {
|
|
13
|
+
requireConfig
|
|
14
|
+
} from "./chunk-ICOMMHKG.js";
|
|
15
|
+
import "./chunk-PCNDP5SX.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/map-import.ts
|
|
18
|
+
import { readFileSync, existsSync } from "fs";
|
|
19
|
+
import ora from "ora";
|
|
20
|
+
function validateString(value, fieldName, maxLength) {
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
throw new Error(`${fieldName} must be a string`);
|
|
23
|
+
}
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
throw new Error(`${fieldName} cannot be empty`);
|
|
27
|
+
}
|
|
28
|
+
if (trimmed.length > maxLength) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${fieldName} exceeds max length of ${maxLength} characters`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(trimmed)) {
|
|
34
|
+
throw new Error(`${fieldName} contains invalid control characters`);
|
|
35
|
+
}
|
|
36
|
+
return trimmed;
|
|
37
|
+
}
|
|
38
|
+
function validateStringArray(value, fieldName, maxItems, itemMaxLength) {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
throw new Error(`${fieldName} must be an array`);
|
|
41
|
+
}
|
|
42
|
+
if (value.length > maxItems) {
|
|
43
|
+
throw new Error(`${fieldName} exceeds max items of ${maxItems}`);
|
|
44
|
+
}
|
|
45
|
+
return value.map((item, idx) => {
|
|
46
|
+
return validateString(
|
|
47
|
+
item,
|
|
48
|
+
`${fieldName}[${idx}]`,
|
|
49
|
+
itemMaxLength
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function validateConfidence(value) {
|
|
54
|
+
if (typeof value !== "string") {
|
|
55
|
+
throw new Error("Confidence must be a string");
|
|
56
|
+
}
|
|
57
|
+
if (!["high", "medium", "low"].includes(value)) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Confidence must be one of: high, medium, low (got: ${value})`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function validatePlanGate(value) {
|
|
65
|
+
if (value === null || value === void 0) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (typeof value !== "string") {
|
|
69
|
+
throw new Error("plan_gate must be a string or null");
|
|
70
|
+
}
|
|
71
|
+
if (!["free", "pro", "team"].includes(value)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`plan_gate must be one of: free, pro, team, null (got: ${value})`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function validateStory(value, actIdx, taskIdx, storyIdx) {
|
|
79
|
+
if (typeof value !== "object" || value === null) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Story [${actIdx}][${taskIdx}][${storyIdx}] must be an object`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const obj = value;
|
|
85
|
+
const story = validateString(
|
|
86
|
+
obj.story,
|
|
87
|
+
`Story [${actIdx}][${taskIdx}][${storyIdx}].story`,
|
|
88
|
+
500
|
|
89
|
+
);
|
|
90
|
+
const evidence = validateString(
|
|
91
|
+
obj.evidence,
|
|
92
|
+
`Story [${actIdx}][${taskIdx}][${storyIdx}].evidence`,
|
|
93
|
+
500
|
|
94
|
+
);
|
|
95
|
+
const confidence = validateConfidence(obj.confidence);
|
|
96
|
+
if (!story.toLowerCase().startsWith("as a ") && !story.toLowerCase().startsWith("as an ")) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Story [${actIdx}][${taskIdx}][${storyIdx}] should follow the format: "As a..., I can... so that..."`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const plan_gate = "plan_gate" in obj ? validatePlanGate(obj.plan_gate) : null;
|
|
102
|
+
const allowedKeys = /* @__PURE__ */ new Set(["story", "evidence", "plan_gate", "confidence"]);
|
|
103
|
+
for (const key of Object.keys(obj)) {
|
|
104
|
+
if (!allowedKeys.has(key)) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Story [${actIdx}][${taskIdx}][${storyIdx}] contains unknown field: ${key}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
story,
|
|
112
|
+
evidence,
|
|
113
|
+
confidence,
|
|
114
|
+
...plan_gate && { plan_gate }
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function validateTask(value, actIdx, taskIdx) {
|
|
118
|
+
if (typeof value !== "object" || value === null) {
|
|
119
|
+
throw new Error(`Task [${actIdx}][${taskIdx}] must be an object`);
|
|
120
|
+
}
|
|
121
|
+
const obj = value;
|
|
122
|
+
const task = validateString(
|
|
123
|
+
obj.task,
|
|
124
|
+
`Task [${actIdx}][${taskIdx}].task`,
|
|
125
|
+
200
|
|
126
|
+
);
|
|
127
|
+
if (!Array.isArray(obj.stories)) {
|
|
128
|
+
throw new Error(`Task [${actIdx}][${taskIdx}].stories must be an array`);
|
|
129
|
+
}
|
|
130
|
+
if (obj.stories.length === 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Task [${actIdx}][${taskIdx}].stories must have at least 1 story`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (obj.stories.length > 50) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Task [${actIdx}][${taskIdx}].stories exceeds max 50 items`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const stories = obj.stories.map(
|
|
141
|
+
(s, sIdx) => validateStory(s, actIdx, taskIdx, sIdx)
|
|
142
|
+
);
|
|
143
|
+
const allowedKeys = /* @__PURE__ */ new Set(["task", "stories"]);
|
|
144
|
+
for (const key of Object.keys(obj)) {
|
|
145
|
+
if (!allowedKeys.has(key)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Task [${actIdx}][${taskIdx}] contains unknown field: ${key}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { task, stories };
|
|
152
|
+
}
|
|
153
|
+
function validateActivity(value, actIdx) {
|
|
154
|
+
if (typeof value !== "object" || value === null) {
|
|
155
|
+
throw new Error(`Activity [${actIdx}] must be an object`);
|
|
156
|
+
}
|
|
157
|
+
const obj = value;
|
|
158
|
+
const activity = validateString(
|
|
159
|
+
obj.activity,
|
|
160
|
+
`Activity [${actIdx}].activity`,
|
|
161
|
+
200
|
|
162
|
+
);
|
|
163
|
+
if (!Array.isArray(obj.tasks)) {
|
|
164
|
+
throw new Error(`Activity [${actIdx}].tasks must be an array`);
|
|
165
|
+
}
|
|
166
|
+
if (obj.tasks.length === 0) {
|
|
167
|
+
throw new Error(`Activity [${actIdx}].tasks must have at least 1 task`);
|
|
168
|
+
}
|
|
169
|
+
if (obj.tasks.length > 30) {
|
|
170
|
+
throw new Error(`Activity [${actIdx}].tasks exceeds max 30 items`);
|
|
171
|
+
}
|
|
172
|
+
const tasks = obj.tasks.map((t, tIdx) => validateTask(t, actIdx, tIdx));
|
|
173
|
+
const allowedKeys = /* @__PURE__ */ new Set(["activity", "tasks"]);
|
|
174
|
+
for (const key of Object.keys(obj)) {
|
|
175
|
+
if (!allowedKeys.has(key)) {
|
|
176
|
+
throw new Error(`Activity [${actIdx}] contains unknown field: ${key}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { activity, tasks };
|
|
180
|
+
}
|
|
181
|
+
function validateStorymapJSON(data) {
|
|
182
|
+
if (typeof data !== "object" || data === null) {
|
|
183
|
+
throw new Error("Storymap must be a JSON object");
|
|
184
|
+
}
|
|
185
|
+
const obj = data;
|
|
186
|
+
if (obj.schema_version !== "1") {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`schema_version must be "1" (got: ${JSON.stringify(obj.schema_version)})`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const product_summary = validateString(
|
|
192
|
+
obj.product_summary,
|
|
193
|
+
"product_summary",
|
|
194
|
+
1e3
|
|
195
|
+
);
|
|
196
|
+
if (!Array.isArray(obj.backbone)) {
|
|
197
|
+
throw new Error("backbone must be an array");
|
|
198
|
+
}
|
|
199
|
+
if (obj.backbone.length < 1 || obj.backbone.length > 20) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
"backbone must have between 1 and 20 activities (got: " + obj.backbone.length + ")"
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const backbone = obj.backbone.map((act, idx) => validateActivity(act, idx));
|
|
205
|
+
let open_questions = [];
|
|
206
|
+
if ("open_questions" in obj && obj.open_questions !== null) {
|
|
207
|
+
open_questions = validateStringArray(
|
|
208
|
+
obj.open_questions,
|
|
209
|
+
"open_questions",
|
|
210
|
+
50,
|
|
211
|
+
500
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
const source = typeof obj.source === "string" ? obj.source.trim() : "unknown";
|
|
215
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
216
|
+
"schema_version",
|
|
217
|
+
"source",
|
|
218
|
+
"product_summary",
|
|
219
|
+
"backbone",
|
|
220
|
+
"data_model",
|
|
221
|
+
"build_order",
|
|
222
|
+
"critical_assumptions",
|
|
223
|
+
"open_questions",
|
|
224
|
+
"stack"
|
|
225
|
+
]);
|
|
226
|
+
for (const key of Object.keys(obj)) {
|
|
227
|
+
if (!allowedKeys.has(key)) {
|
|
228
|
+
throw new Error(`Unknown top-level field: ${key}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const result = {
|
|
232
|
+
schema_version: "1",
|
|
233
|
+
source,
|
|
234
|
+
product_summary,
|
|
235
|
+
backbone,
|
|
236
|
+
open_questions
|
|
237
|
+
};
|
|
238
|
+
if (Array.isArray(obj.data_model)) result.data_model = obj.data_model;
|
|
239
|
+
if (Array.isArray(obj.build_order)) result.build_order = obj.build_order;
|
|
240
|
+
if (Array.isArray(obj.critical_assumptions)) result.critical_assumptions = obj.critical_assumptions;
|
|
241
|
+
if (obj.stack && typeof obj.stack === "object") result.stack = obj.stack;
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
function printSummary(storymap) {
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(" \x1B[1mStorymap Summary\x1B[0m\n");
|
|
247
|
+
console.log(` \x1B[36mProduct:\x1B[0m ${storymap.product_summary}`);
|
|
248
|
+
console.log();
|
|
249
|
+
let totalTasks = 0;
|
|
250
|
+
let totalStories = 0;
|
|
251
|
+
const confidenceCounts = { high: 0, medium: 0, low: 0 };
|
|
252
|
+
for (const activity of storymap.backbone) {
|
|
253
|
+
for (const task of activity.tasks) {
|
|
254
|
+
totalTasks++;
|
|
255
|
+
totalStories += task.stories.length;
|
|
256
|
+
for (const story of task.stories) {
|
|
257
|
+
confidenceCounts[story.confidence]++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
console.log(
|
|
262
|
+
` \x1B[33mActivities:\x1B[0m ${storymap.backbone.length} | \x1B[33mTasks:\x1B[0m ${totalTasks} | \x1B[33mStories:\x1B[0m ${totalStories}`
|
|
263
|
+
);
|
|
264
|
+
console.log(` \x1B[32mHigh confidence:\x1B[0m ${confidenceCounts.high}`);
|
|
265
|
+
console.log(` \x1B[33mMedium confidence:\x1B[0m ${confidenceCounts.medium}`);
|
|
266
|
+
console.log(` \x1B[31mLow confidence:\x1B[0m ${confidenceCounts.low}`);
|
|
267
|
+
if (storymap.open_questions && storymap.open_questions.length > 0) {
|
|
268
|
+
console.log(
|
|
269
|
+
` \x1B[36mOpen questions:\x1B[0m ${storymap.open_questions.length}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
console.log();
|
|
273
|
+
}
|
|
274
|
+
async function mapImport(filePath, opts) {
|
|
275
|
+
const config = requireConfig();
|
|
276
|
+
if (!filePath) {
|
|
277
|
+
console.error(" File path is required");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!existsSync(filePath)) {
|
|
281
|
+
console.error(` File not found: ${filePath}`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(`
|
|
285
|
+
Importing storymap from ${filePath}...
|
|
286
|
+
`);
|
|
287
|
+
const spinner = ora(" Validating storymap...").start();
|
|
288
|
+
try {
|
|
289
|
+
const jsonText = readFileSync(filePath, "utf-8");
|
|
290
|
+
let storymap;
|
|
291
|
+
try {
|
|
292
|
+
storymap = validateStorymapJSON(JSON.parse(jsonText));
|
|
293
|
+
} catch (validationErr) {
|
|
294
|
+
spinner.fail(` Validation failed: ${validationErr.message}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
spinner.succeed(" Storymap is valid\n");
|
|
298
|
+
printSummary(storymap);
|
|
299
|
+
if (!opts?.yes) {
|
|
300
|
+
const doImport = await confirm(
|
|
301
|
+
` Import this storymap into your story map?`
|
|
302
|
+
);
|
|
303
|
+
if (!doImport) {
|
|
304
|
+
console.log(" Import cancelled.\n");
|
|
305
|
+
closePrompt();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const uploadSpinner = ora(" Uploading storymap...").start();
|
|
310
|
+
const res = await fetch(apiUrl(config, "/map-import"), {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
body: JSON.stringify(storymap)
|
|
314
|
+
});
|
|
315
|
+
if (!res.ok) {
|
|
316
|
+
if (isPlanGated(res.status)) {
|
|
317
|
+
uploadSpinner.fail(" Storymap import is not available on your plan");
|
|
318
|
+
printUpgradeMessage();
|
|
319
|
+
closePrompt();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (res.status === 401) {
|
|
323
|
+
uploadSpinner.fail(" Sync token expired or invalid");
|
|
324
|
+
console.error(
|
|
325
|
+
"\n Your sync token has expired. Run \x1B[36mstc link\x1B[0m to generate a fresh one.\n"
|
|
326
|
+
);
|
|
327
|
+
closePrompt();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
uploadSpinner.fail(" Upload failed");
|
|
331
|
+
const errText = await res.text();
|
|
332
|
+
console.error(` ${errText.slice(0, 200)}`);
|
|
333
|
+
closePrompt();
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const result = await res.json();
|
|
337
|
+
uploadSpinner.succeed(
|
|
338
|
+
` \u2713 Successfully imported storymap
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
console.log(
|
|
342
|
+
` \x1B[32m${result.stories}\x1B[0m stories across \x1B[32m${result.activities}\x1B[0m activities and \x1B[32m${result.steps}\x1B[0m steps`
|
|
343
|
+
);
|
|
344
|
+
if (result.openQuestions > 0) {
|
|
345
|
+
console.log(
|
|
346
|
+
` \x1B[36m${result.openQuestions}\x1B[0m open questions for you to review in the web UI`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
closePrompt();
|
|
351
|
+
} catch (err) {
|
|
352
|
+
spinner.fail(` Error: ${err.message}`);
|
|
353
|
+
closePrompt();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export {
|
|
357
|
+
mapImport
|
|
358
|
+
};
|