intentdocs 0.10.1 → 0.11.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 +1 -1
- package/dist/chunk-64I7RLP3.js +171 -0
- package/dist/{chunk-6X4BORMW.js → chunk-65EDM7QH.js} +1 -1
- package/dist/{chunk-F52HPVNQ.js → chunk-BNEU6TLJ.js} +54 -20
- package/dist/chunk-GFKZ5CRL.js +12 -0
- package/dist/{chunk-NHNBCNND.js → chunk-ID4B5RZ2.js} +37 -16
- package/dist/{chunk-PUGYRSMK.js → chunk-IORMKLAQ.js} +122 -65
- package/dist/{chunk-JX2RLGOM.js → chunk-K5SNF6CG.js} +11 -0
- package/dist/{chunk-34JCN66H.js → chunk-KTGQO3AE.js} +24 -1
- package/dist/{ci-XCOXFTLB.js → ci-NOCV3XUV.js} +2 -2
- package/dist/{complexity-X3IJKNNV.js → complexity-OJVM37NF.js} +2 -2
- package/dist/{import-prd-IJTM6FZE.js → import-prd-5FCVXCYO.js} +12 -5
- package/dist/index.js +344 -326
- package/dist/{keys-UVXCCWCS.js → keys-2W5V56FS.js} +30 -9
- package/dist/{log-XIH4BGFL.js → log-DFCCAAJN.js} +21 -5
- package/dist/{map-extract-V6D27I3H.js → map-extract-YUCFLGJS.js} +1 -1
- package/dist/{map-import-LORTI4K4.js → map-import-7RN2NEUZ.js} +5 -4
- package/dist/push-33V7ANJW.js +462 -0
- package/dist/{research-CO3W7PLC.js → research-SHO5SXVQ.js} +2 -2
- package/dist/scope-RRGJHS2G.js +13 -0
- package/dist/{setup-WSYIG2CR.js → setup-UILQVYIB.js} +5 -5
- package/package.json +6 -5
- package/dist/scope-ZZLKQEF5.js +0 -11
|
@@ -9,6 +9,16 @@ import "./chunk-UYTVTYNB.js";
|
|
|
9
9
|
|
|
10
10
|
// src/commands/keys.ts
|
|
11
11
|
var KEYS_DOC_URL = "https://console.anthropic.com/settings/keys";
|
|
12
|
+
var XAI_KEYS_URL = "https://console.x.ai/";
|
|
13
|
+
var ANTHROPIC_RE = /^sk-ant-[A-Za-z0-9_-]{24,250}$/;
|
|
14
|
+
var XAI_RE = /^xai-[A-Za-z0-9_-]{20,250}$/;
|
|
15
|
+
var OPENAI_PROJ_RE = /^sk-proj-[A-Za-z0-9_-]{20,250}$/;
|
|
16
|
+
var OPENAI_SK_RE = /^sk-[A-Za-z0-9_-]{20,250}$/;
|
|
17
|
+
function acceptedLocally(value) {
|
|
18
|
+
if (ANTHROPIC_RE.test(value) || XAI_RE.test(value) || OPENAI_PROJ_RE.test(value)) return true;
|
|
19
|
+
if (!OPENAI_SK_RE.test(value)) return false;
|
|
20
|
+
return !/^(sk-ant-|sk-proj-|sk-admin-|sk-svcacct-)/.test(value);
|
|
21
|
+
}
|
|
12
22
|
async function byokRequest(method, body) {
|
|
13
23
|
const auth = await requireValidAuth();
|
|
14
24
|
const res = await fetch(`${auth.apiBase}/api/settings/byok`, {
|
|
@@ -25,17 +35,22 @@ async function byokRequest(method, body) {
|
|
|
25
35
|
async function keysSet(key) {
|
|
26
36
|
let value = key?.trim();
|
|
27
37
|
if (!value) {
|
|
28
|
-
console.log("\n Optionally add
|
|
38
|
+
console.log("\n Optionally add an Anthropic, xAI, or OpenAI API key to unlock AI features on any plan.");
|
|
29
39
|
console.log(" Your key is encrypted at rest and only used for your AI operations.");
|
|
30
|
-
console.log(` Get one at: ${KEYS_DOC_URL}
|
|
31
|
-
`);
|
|
32
|
-
|
|
40
|
+
console.log(` Anthropic keys start with sk-ant-. Get one at: ${KEYS_DOC_URL}`);
|
|
41
|
+
console.log(` xAI keys start with xai-. Get one at: ${XAI_KEYS_URL}`);
|
|
42
|
+
console.log(" OpenAI keys start with sk- or sk-proj-. Get one at: https://platform.openai.com/api-keys\n");
|
|
43
|
+
value = (await ask(" API key (sk-ant-..., xai-..., sk-..., or sk-proj-...): ")).trim();
|
|
33
44
|
closePrompt();
|
|
34
45
|
}
|
|
35
46
|
if (!value) {
|
|
36
47
|
console.error(" No key provided. Run `npx intentdocs keys set` to try again.");
|
|
37
48
|
process.exit(1);
|
|
38
49
|
}
|
|
50
|
+
if (!acceptedLocally(value)) {
|
|
51
|
+
console.error("\n \u2717 That key is not accepted. Use an sk-ant- key, an xai- key, an sk- key, or an sk-proj- key.");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
39
54
|
try {
|
|
40
55
|
const { ok, data } = await byokRequest("POST", { key: value });
|
|
41
56
|
if (!ok) {
|
|
@@ -59,12 +74,18 @@ async function keysStatus() {
|
|
|
59
74
|
process.exit(1);
|
|
60
75
|
}
|
|
61
76
|
if (data?.enabled) {
|
|
77
|
+
const anthropic = data.anthropic?.mask || "not set";
|
|
78
|
+
const xai = data.xai?.mask || "not set";
|
|
79
|
+
const openai = data.openai?.mask || "not set";
|
|
62
80
|
console.log(`
|
|
63
|
-
Anthropic API key: ${
|
|
64
|
-
console.log(
|
|
81
|
+
Anthropic API key: ${anthropic}`);
|
|
82
|
+
console.log(` xAI API key: ${xai}`);
|
|
83
|
+
console.log(` OpenAI API key: ${openai}`);
|
|
84
|
+
if (data.active_provider) console.log(` Active: ${data.active_provider}`);
|
|
85
|
+
console.log(" AI features run on your active key.\n");
|
|
65
86
|
} else {
|
|
66
|
-
console.log("\n No
|
|
67
|
-
console.log(" Add
|
|
87
|
+
console.log("\n No API key set.");
|
|
88
|
+
console.log(" Add an sk-ant-, xai-, sk-, or sk-proj- key with `npx intentdocs keys set` to unlock AI features on any plan.\n");
|
|
68
89
|
}
|
|
69
90
|
} catch {
|
|
70
91
|
console.error(" \u2717 Could not reach intentdocs. Check your connection and try again.");
|
|
@@ -78,7 +99,7 @@ async function keysRemove() {
|
|
|
78
99
|
console.error(" \u2717 Failed to remove your key.");
|
|
79
100
|
process.exit(1);
|
|
80
101
|
}
|
|
81
|
-
console.log("\n Key removed. AI features fall back
|
|
102
|
+
console.log("\n Key removed. Revoke it at the provider. AI features fall back when no key remains.\n");
|
|
82
103
|
} catch {
|
|
83
104
|
console.error(" \u2717 Could not reach intentdocs. Check your connection and try again.");
|
|
84
105
|
process.exit(1);
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendToNotes
|
|
3
|
+
} from "./chunk-GFKZ5CRL.js";
|
|
1
4
|
import {
|
|
2
5
|
appendNoteToContext,
|
|
3
6
|
storyContextPath
|
|
4
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-BNEU6TLJ.js";
|
|
5
8
|
import {
|
|
6
9
|
matchesStoryRef
|
|
7
10
|
} from "./chunk-LQTPZL2Y.js";
|
|
@@ -9,11 +12,12 @@ import {
|
|
|
9
12
|
amendDecision,
|
|
10
13
|
apiFetch,
|
|
11
14
|
listDecisions,
|
|
15
|
+
listStories,
|
|
12
16
|
retractDecision
|
|
13
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KTGQO3AE.js";
|
|
14
18
|
import {
|
|
15
19
|
requireConfig
|
|
16
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-K5SNF6CG.js";
|
|
17
21
|
import {
|
|
18
22
|
ask,
|
|
19
23
|
closePrompt,
|
|
@@ -38,6 +42,18 @@ async function logDecision(title, opts) {
|
|
|
38
42
|
closePrompt();
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
45
|
+
let storyId;
|
|
46
|
+
if (opts?.story) {
|
|
47
|
+
const { stories } = await listStories(config);
|
|
48
|
+
const match = stories.find((s) => matchesStoryRef(s, opts.story));
|
|
49
|
+
if (!match) {
|
|
50
|
+
console.error(` No story found matching "${opts.story}" \u2014 decision not logged.`);
|
|
51
|
+
closePrompt();
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
storyId = match.id;
|
|
56
|
+
}
|
|
41
57
|
const res = await apiFetch(config, "/decisions", {
|
|
42
58
|
method: "POST",
|
|
43
59
|
headers: { "Content-Type": "application/json" },
|
|
@@ -45,7 +61,7 @@ async function logDecision(title, opts) {
|
|
|
45
61
|
title: title.trim(),
|
|
46
62
|
description: description.trim(),
|
|
47
63
|
category: opts?.category ?? "technical",
|
|
48
|
-
story_id:
|
|
64
|
+
story_id: storyId
|
|
49
65
|
})
|
|
50
66
|
});
|
|
51
67
|
if (res.ok) {
|
|
@@ -182,7 +198,7 @@ async function logNote(storyId, note) {
|
|
|
182
198
|
method: "PATCH",
|
|
183
199
|
headers: { "Content-Type": "application/json" },
|
|
184
200
|
body: JSON.stringify({
|
|
185
|
-
stories: [{ id: match.id, notes: note
|
|
201
|
+
stories: [{ id: match.id, notes: appendToNotes(match.notes, note) }]
|
|
186
202
|
})
|
|
187
203
|
});
|
|
188
204
|
if (res.ok) {
|
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
apiUrl,
|
|
7
7
|
syncHeaders
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KTGQO3AE.js";
|
|
9
9
|
import {
|
|
10
10
|
requireConfig
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-K5SNF6CG.js";
|
|
12
12
|
import {
|
|
13
13
|
closePrompt,
|
|
14
14
|
confirm
|
|
@@ -322,11 +322,12 @@ async function mapImport(filePath, opts) {
|
|
|
322
322
|
return;
|
|
323
323
|
}
|
|
324
324
|
if (res.status === 401) {
|
|
325
|
-
uploadSpinner.fail("
|
|
325
|
+
uploadSpinner.fail(" Your login is no longer valid");
|
|
326
326
|
console.error(
|
|
327
|
-
"\n
|
|
327
|
+
"\n Run \x1B[36mnpx intentdocs login\x1B[0m to log in again, then re-run the import.\n"
|
|
328
328
|
);
|
|
329
329
|
closePrompt();
|
|
330
|
+
process.exitCode = 1;
|
|
330
331
|
return;
|
|
331
332
|
}
|
|
332
333
|
uploadSpinner.fail(" Upload failed");
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import {
|
|
2
|
+
login
|
|
3
|
+
} from "./chunk-64I7RLP3.js";
|
|
4
|
+
import {
|
|
5
|
+
checkoutMismatch
|
|
6
|
+
} from "./chunk-KTGQO3AE.js";
|
|
7
|
+
import {
|
|
8
|
+
checkoutConfig,
|
|
9
|
+
readConfig,
|
|
10
|
+
writeConfig
|
|
11
|
+
} from "./chunk-K5SNF6CG.js";
|
|
12
|
+
import {
|
|
13
|
+
choose,
|
|
14
|
+
closePrompt,
|
|
15
|
+
isInteractive
|
|
16
|
+
} from "./chunk-WRRUCCVZ.js";
|
|
17
|
+
import {
|
|
18
|
+
authenticatedFetch,
|
|
19
|
+
getAuth
|
|
20
|
+
} from "./chunk-XZE2XP6O.js";
|
|
21
|
+
import "./chunk-UYTVTYNB.js";
|
|
22
|
+
|
|
23
|
+
// src/commands/push.ts
|
|
24
|
+
import { exec } from "child_process";
|
|
25
|
+
|
|
26
|
+
// src/lib/intent-folder.ts
|
|
27
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
28
|
+
import { basename, dirname, join, relative } from "path";
|
|
29
|
+
import { parse } from "yaml";
|
|
30
|
+
var VISION_SECTIONS = ["Users", "Problem", "Switch", "First", "Proof"];
|
|
31
|
+
var STORY_ID = /^[A-Z][A-Z0-9]{0,9}-[0-9]+$/;
|
|
32
|
+
var PRIORITIES = ["must", "should", "could", "wont"];
|
|
33
|
+
var STATUS = { todo: "todo", in_progress: "in_progress", needs_rework: "needs_rework", done: "dev_complete" };
|
|
34
|
+
function findIntentDir(start) {
|
|
35
|
+
return existsSync(join(start, "storymap.yaml")) ? start : join(start, "intent");
|
|
36
|
+
}
|
|
37
|
+
function walk(dir, ext) {
|
|
38
|
+
if (!existsSync(dir)) return [];
|
|
39
|
+
return readdirSync(dir).sort().flatMap((f) => {
|
|
40
|
+
const p = join(dir, f);
|
|
41
|
+
if (statSync(p).isDirectory()) return walk(p, ext);
|
|
42
|
+
return p.endsWith(ext) ? [p] : [];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function frontMatter(text) {
|
|
46
|
+
const m = /^---\n([\s\S]*?)\n---\n?/.exec(text);
|
|
47
|
+
if (!m) return null;
|
|
48
|
+
const meta = parse(m[1]) ?? {};
|
|
49
|
+
return { meta, body: text.slice(m[0].length) };
|
|
50
|
+
}
|
|
51
|
+
function sections(body) {
|
|
52
|
+
return [...body.matchAll(/^## (.+)\n([\s\S]*?)(?=^## |(?![\s\S]))/gm)].map((s) => ({ title: s[1].trim(), text: s[2].trim() }));
|
|
53
|
+
}
|
|
54
|
+
function tagsOf(line) {
|
|
55
|
+
return line.trim().split(/\s+/).filter((t) => t.startsWith("@")).map((t) => t.slice(1));
|
|
56
|
+
}
|
|
57
|
+
var EFFORTS = ["XS", "S", "M", "L", "XL"];
|
|
58
|
+
function effortOf(tags) {
|
|
59
|
+
const tag = tags.find((t) => t.startsWith("effort-"))?.slice("effort-".length).toUpperCase();
|
|
60
|
+
if (!tag) return null;
|
|
61
|
+
return EFFORTS.includes(tag) ? tag : "M";
|
|
62
|
+
}
|
|
63
|
+
function priorityOf(tags) {
|
|
64
|
+
return tags.find((t) => PRIORITIES.includes(t)) ?? null;
|
|
65
|
+
}
|
|
66
|
+
function stepsSentence(steps) {
|
|
67
|
+
return steps.map(({ keyword, text }, i) => {
|
|
68
|
+
const k = keyword.toLowerCase();
|
|
69
|
+
if (k === "and" || k === "but") return `${k} ${text}`;
|
|
70
|
+
return i === 0 ? `${keyword} ${text}` : `, ${k} ${text}`;
|
|
71
|
+
}).join(" ").replace(/ ,/g, ",");
|
|
72
|
+
}
|
|
73
|
+
function parseFeature(text) {
|
|
74
|
+
const lines = text.split("\n");
|
|
75
|
+
const featureAt = lines.findIndex((l) => l.trim().startsWith("Feature:"));
|
|
76
|
+
if (featureAt < 0) return null;
|
|
77
|
+
const tags = lines.slice(0, featureAt).flatMap(tagsOf);
|
|
78
|
+
const title = lines[featureAt].trim().slice("Feature:".length).trim();
|
|
79
|
+
let asA = "";
|
|
80
|
+
let iWant = "";
|
|
81
|
+
let soThat = "";
|
|
82
|
+
const criteria = [];
|
|
83
|
+
let pendingTags = [];
|
|
84
|
+
let current = null;
|
|
85
|
+
const flush = () => {
|
|
86
|
+
if (current) criteria.push({ label: current.label, title: current.title, text: stepsSentence(current.steps), priority: current.priority });
|
|
87
|
+
current = null;
|
|
88
|
+
};
|
|
89
|
+
for (const raw of lines.slice(featureAt + 1)) {
|
|
90
|
+
const line = raw.trim();
|
|
91
|
+
if (!line || line.startsWith("#")) continue;
|
|
92
|
+
if (line.startsWith("@")) {
|
|
93
|
+
pendingTags.push(...tagsOf(line));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const scenario = /^Scenario(?: Outline)?:\s*(.*)$/.exec(line);
|
|
97
|
+
if (scenario) {
|
|
98
|
+
flush();
|
|
99
|
+
const name = scenario[1].trim();
|
|
100
|
+
const label = pendingTags.find((t) => /^AC\d+$/.test(t)) ?? /^(AC\d+)\b/.exec(name)?.[1] ?? "";
|
|
101
|
+
current = { label, title: name.replace(/^AC\d+\s*[—–-]\s*/, ""), priority: priorityOf(pendingTags), steps: [] };
|
|
102
|
+
pendingTags = [];
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const step = /^(Given|When|Then|And|But)\s+(.*)$/.exec(line);
|
|
106
|
+
if (step && current) {
|
|
107
|
+
current.steps.push({ keyword: step[1], text: step[2] });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!current) {
|
|
111
|
+
const story = /^(As (?:a|an|the)\b.*|I want\b.*|So that\b.*)$/i.exec(line);
|
|
112
|
+
if (story) {
|
|
113
|
+
const s = story[1];
|
|
114
|
+
if (/^as /i.test(s)) asA = s.replace(/^As (a|an|the)\s+/i, "");
|
|
115
|
+
else if (/^i want/i.test(s)) iWant = s.replace(/^I want( to)?\s+/i, "");
|
|
116
|
+
else soThat = s.replace(/^So that\s+/i, "");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
flush();
|
|
121
|
+
return { tags, title, asA, iWant, soThat, criteria };
|
|
122
|
+
}
|
|
123
|
+
function parsePlanSteps(body) {
|
|
124
|
+
return sections(body).map(({ title, text }) => {
|
|
125
|
+
const field = (name) => new RegExp(`^\\*\\*${name}:\\*\\*\\s*(.*)$`, "m").exec(text)?.[1]?.trim() ?? "";
|
|
126
|
+
const list = (name) => field(name).split(",").map((s) => s.trim()).filter(Boolean);
|
|
127
|
+
const notes = text.split("\n").filter((l) => !/^\*\*(Why|Criteria|Files|Tests|Needs you):\*\*/.test(l.trim())).join("\n").trim();
|
|
128
|
+
const needs = field("Needs you");
|
|
129
|
+
return {
|
|
130
|
+
title: title.replace(/^\d+\.\s*/, ""),
|
|
131
|
+
why: field("Why"),
|
|
132
|
+
criteria: list("Criteria"),
|
|
133
|
+
files: list("Files"),
|
|
134
|
+
tests: list("Tests"),
|
|
135
|
+
needs_you: needs ? { question: needs } : null,
|
|
136
|
+
agent_notes: notes
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function parseDecisions(text) {
|
|
141
|
+
const uncommented = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
142
|
+
return sections(uncommented).map(({ title, text: body }) => {
|
|
143
|
+
const heading = /^(\d{4}-\d{2}-\d{2})\s*[—–-]\s*(.*?)(?:\s+\(([A-Z][A-Z0-9]{0,9}-\d+)\))?$/.exec(title);
|
|
144
|
+
const category = /^\*\*Category:\*\*\s*(.*)$/m.exec(body)?.[1]?.trim() ?? "architecture";
|
|
145
|
+
const description = body.split("\n").filter((l) => !/^\*\*(Category|Supersedes):\*\*/.test(l.trim())).join("\n").trim();
|
|
146
|
+
return {
|
|
147
|
+
title: heading?.[2] ?? title,
|
|
148
|
+
description,
|
|
149
|
+
category,
|
|
150
|
+
date: heading?.[1] ?? "",
|
|
151
|
+
story: heading?.[3] ?? null
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function readIntentFolder(start) {
|
|
156
|
+
const dir = findIntentDir(start);
|
|
157
|
+
const errors = [];
|
|
158
|
+
const fail = () => ({ ok: false, dir, errors });
|
|
159
|
+
const file = (name) => join(dir, name);
|
|
160
|
+
if (!existsSync(dir)) return { ok: false, dir, errors: ["No intent/ folder here. Run `npx nikos init` first, or push from the repo root."] };
|
|
161
|
+
for (const name of ["constitution.md", "storymap.yaml", "state.yaml"]) {
|
|
162
|
+
if (!existsSync(file(name))) errors.push(`${name} is missing`);
|
|
163
|
+
}
|
|
164
|
+
if (errors.length) return fail();
|
|
165
|
+
let storymap;
|
|
166
|
+
let state;
|
|
167
|
+
try {
|
|
168
|
+
storymap = parse(readFileSync(file("storymap.yaml"), "utf-8")) ?? {};
|
|
169
|
+
state = parse(readFileSync(file("state.yaml"), "utf-8")) ?? {};
|
|
170
|
+
} catch (err) {
|
|
171
|
+
errors.push(`invalid YAML: ${err.message.split("\n")[0]}`);
|
|
172
|
+
return fail();
|
|
173
|
+
}
|
|
174
|
+
const constitution = frontMatter(readFileSync(file("constitution.md"), "utf-8"));
|
|
175
|
+
const vision = {};
|
|
176
|
+
if (!constitution) {
|
|
177
|
+
errors.push("constitution.md has no front matter (name and idea)");
|
|
178
|
+
} else {
|
|
179
|
+
for (const key of ["name", "idea"]) {
|
|
180
|
+
if (typeof constitution.meta[key] !== "string" || !constitution.meta[key].trim()) errors.push(`constitution.md front matter is missing '${key}'`);
|
|
181
|
+
}
|
|
182
|
+
const found = sections(constitution.body);
|
|
183
|
+
for (const name of VISION_SECTIONS) {
|
|
184
|
+
const text = found.find((s) => s.title === name)?.text ?? "";
|
|
185
|
+
if (!text) errors.push(`constitution.md ## ${name} is missing or empty`);
|
|
186
|
+
vision[name.toLowerCase()] = text;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!storymap.key || !/^[A-Z]{2,5}$/.test(storymap.key)) errors.push("storymap.yaml needs a key of 2\u20135 capital letters, like HT");
|
|
190
|
+
const activities = new Map((storymap.activities ?? []).map((a) => [a.id, a.title]));
|
|
191
|
+
const releaseOf = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const r of storymap.releases ?? []) for (const id of r.stories ?? []) releaseOf.set(id, r.number);
|
|
193
|
+
const features = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const path of walk(file("features"), ".feature")) {
|
|
195
|
+
const where = relative(dir, path);
|
|
196
|
+
const parsed = parseFeature(readFileSync(path, "utf-8"));
|
|
197
|
+
if (!parsed) {
|
|
198
|
+
errors.push(`${where} has no Feature:`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const ids = parsed.tags.filter((t) => STORY_ID.test(t));
|
|
202
|
+
if (ids.length !== 1) {
|
|
203
|
+
errors.push(`${where} needs exactly one story id tag (found ${ids.length})`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
features.set(ids[0], { path, parsed });
|
|
207
|
+
}
|
|
208
|
+
const stories = [];
|
|
209
|
+
for (const [key, release] of releaseOf) {
|
|
210
|
+
const feature = features.get(key);
|
|
211
|
+
if (!feature) {
|
|
212
|
+
errors.push(`${key} is in storymap.yaml but has no feature file`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const { parsed, path } = feature;
|
|
216
|
+
const where = relative(dir, path);
|
|
217
|
+
const activityId = parsed.tags.find((t) => t.startsWith("activity-"))?.slice("activity-".length) ?? basename(dirname(path));
|
|
218
|
+
const activity = activities.get(activityId);
|
|
219
|
+
if (!activity) errors.push(`${where}: activity '${activityId}' is not in storymap.yaml`);
|
|
220
|
+
if (!parsed.asA || !parsed.iWant || !parsed.soThat) errors.push(`${where}: needs "As a / I want / So that" under Feature:`);
|
|
221
|
+
if (!parsed.criteria.length) errors.push(`${where}: has no scenarios`);
|
|
222
|
+
for (const c of parsed.criteria) if (!c.label) errors.push(`${where}: scenario "${c.title}" has no @AC label`);
|
|
223
|
+
const entry = state.stories?.[key];
|
|
224
|
+
if (!entry) errors.push(`state.yaml has no entry for ${key}`);
|
|
225
|
+
const status = STATUS[entry?.status ?? "todo"];
|
|
226
|
+
if (!status) errors.push(`state.yaml ${key} has unknown status '${entry?.status}'`);
|
|
227
|
+
stories.push({
|
|
228
|
+
key,
|
|
229
|
+
title: parsed.title,
|
|
230
|
+
as_a: parsed.asA,
|
|
231
|
+
i_want: parsed.iWant,
|
|
232
|
+
so_that: parsed.soThat,
|
|
233
|
+
priority: priorityOf(parsed.tags),
|
|
234
|
+
release,
|
|
235
|
+
activity: activity ?? activityId,
|
|
236
|
+
activity_id: activityId,
|
|
237
|
+
effort: effortOf(parsed.tags),
|
|
238
|
+
status: status ?? "todo",
|
|
239
|
+
blockers: entry?.blockers ?? [],
|
|
240
|
+
claimed_by: entry?.claimed_by ?? null,
|
|
241
|
+
plan_version: entry?.plan_version ?? null,
|
|
242
|
+
criteria: parsed.criteria
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
for (const key of features.keys()) if (!releaseOf.has(key)) errors.push(`${key} has a feature file but is in no release in storymap.yaml`);
|
|
246
|
+
const plans = [];
|
|
247
|
+
for (const path of walk(file("plans"), ".md")) {
|
|
248
|
+
const m = /^([A-Z][A-Z0-9]{0,9}-\d+)\.v(\d+)\.md$/.exec(basename(path));
|
|
249
|
+
if (!m) continue;
|
|
250
|
+
const fm = frontMatter(readFileSync(path, "utf-8"));
|
|
251
|
+
if (!fm) {
|
|
252
|
+
errors.push(`plans/${basename(path)} has no front matter`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
plans.push({
|
|
256
|
+
story: m[1],
|
|
257
|
+
version: Number(m[2]),
|
|
258
|
+
summary: typeof fm.meta.summary === "string" ? fm.meta.summary : "",
|
|
259
|
+
risk: typeof fm.meta.risk === "string" ? fm.meta.risk : null,
|
|
260
|
+
introduces: Array.isArray(fm.meta.introduces) ? fm.meta.introduces.map(String) : [],
|
|
261
|
+
steps: parsePlanSteps(fm.body)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
plans.sort((a, b) => a.story.localeCompare(b.story) || a.version - b.version);
|
|
265
|
+
const decisions = existsSync(file("decisions.md")) ? parseDecisions(readFileSync(file("decisions.md"), "utf-8")) : [];
|
|
266
|
+
if (errors.length) return fail();
|
|
267
|
+
return {
|
|
268
|
+
ok: true,
|
|
269
|
+
dir,
|
|
270
|
+
payload: {
|
|
271
|
+
format_version: 1,
|
|
272
|
+
project: { name: constitution.meta.name, idea: constitution.meta.idea, key: storymap.key, vision },
|
|
273
|
+
releases: (storymap.releases ?? []).map((r) => ({ number: r.number, title: r.title ?? `Release ${r.number}`, stories: r.stories ?? [] })),
|
|
274
|
+
stories,
|
|
275
|
+
plans,
|
|
276
|
+
decisions,
|
|
277
|
+
run_queue: state.run?.queue ?? []
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/lib/push-api.ts
|
|
283
|
+
var PUSH_ENDPOINTS = {
|
|
284
|
+
listWorkspaces: "/api/cli/workspaces",
|
|
285
|
+
createPersonalWorkspace: "/api/cli/workspaces/personal",
|
|
286
|
+
createProject: "/api/cli/push",
|
|
287
|
+
updateProject: (projectId) => `/api/cli/push/${encodeURIComponent(projectId)}`,
|
|
288
|
+
projectMaps: (projectId) => `/api/cli/projects/${encodeURIComponent(projectId)}/maps`
|
|
289
|
+
};
|
|
290
|
+
var PushApiError = class extends Error {
|
|
291
|
+
constructor(message, status, code = null, problems = [], projectId = null) {
|
|
292
|
+
super(message);
|
|
293
|
+
this.status = status;
|
|
294
|
+
this.code = code;
|
|
295
|
+
this.problems = problems;
|
|
296
|
+
this.projectId = projectId;
|
|
297
|
+
this.name = "PushApiError";
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
async function call(fetcher, path, init, what) {
|
|
301
|
+
const res = await fetcher(path, init);
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const body = await res.json().catch(() => ({}));
|
|
304
|
+
const nested = typeof body.error === "object" && body.error ? body.error : null;
|
|
305
|
+
const said = body.message ?? (nested ? nested.hint ?? nested.message : void 0);
|
|
306
|
+
throw new PushApiError(
|
|
307
|
+
said ?? `Couldn't ${what} (the server returned ${res.status}).`,
|
|
308
|
+
res.status,
|
|
309
|
+
typeof body.error === "string" ? body.error : null,
|
|
310
|
+
Array.isArray(body.problems) ? body.problems : [],
|
|
311
|
+
body.project_id ?? null
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return await res.json();
|
|
315
|
+
}
|
|
316
|
+
var json = (method, body) => body === void 0 ? { method } : { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) };
|
|
317
|
+
function pushApi(fetcher = authenticatedFetch) {
|
|
318
|
+
return {
|
|
319
|
+
listWorkspaces: () => call(fetcher, PUSH_ENDPOINTS.listWorkspaces, json("GET"), "list your workspaces").then((r) => r.workspaces),
|
|
320
|
+
createPersonalWorkspace: () => call(fetcher, PUSH_ENDPOINTS.createPersonalWorkspace, json("POST"), "open your personal workspace").then(
|
|
321
|
+
(r) => r.workspace
|
|
322
|
+
),
|
|
323
|
+
createProject: (workspaceId, payload) => call(fetcher, PUSH_ENDPOINTS.createProject, json("POST", { workspace_id: workspaceId, intent: payload }), "create the project"),
|
|
324
|
+
updateProject: (projectId, mapId, payload) => call(fetcher, PUSH_ENDPOINTS.updateProject(projectId), json("PUT", { map_id: mapId, intent: payload }), "update the project"),
|
|
325
|
+
/** The project's story maps, newest first. */
|
|
326
|
+
projectMaps: (projectId) => call(fetcher, PUSH_ENDPOINTS.projectMaps(projectId), json("GET"), "find the project's story map").then(
|
|
327
|
+
(r) => r.maps
|
|
328
|
+
)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/commands/push.ts
|
|
333
|
+
var PushError = class extends Error {
|
|
334
|
+
constructor(message, details = []) {
|
|
335
|
+
super(message);
|
|
336
|
+
this.details = details;
|
|
337
|
+
this.name = "PushError";
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
function summary(p) {
|
|
341
|
+
const criteria = p.stories.reduce((n, s) => n + s.criteria.length, 0);
|
|
342
|
+
return `${p.stories.length} stories, ${criteria} criteria, ${p.releases.length} releases, ${p.plans.length} plans, ${p.decisions.length} decisions`;
|
|
343
|
+
}
|
|
344
|
+
async function pickWorkspace(deps, auth) {
|
|
345
|
+
const workspaces = await deps.api.listWorkspaces();
|
|
346
|
+
if (auth.binding) {
|
|
347
|
+
const { orgId, workspaceName } = auth.binding;
|
|
348
|
+
return workspaces.find((w) => w.orgId === orgId) ?? { id: orgId, orgId, name: workspaceName ?? null, personal: false };
|
|
349
|
+
}
|
|
350
|
+
if (workspaces.length === 0) return deps.api.createPersonalWorkspace();
|
|
351
|
+
if (workspaces.length === 1) return workspaces[0];
|
|
352
|
+
const personal = Math.max(0, workspaces.findIndex((w) => w.personal));
|
|
353
|
+
return workspaces[await deps.chooseWorkspace(workspaces, personal)] ?? workspaces[personal];
|
|
354
|
+
}
|
|
355
|
+
function pushNotes(result) {
|
|
356
|
+
const notes = [];
|
|
357
|
+
const statuses = result.ignored?.statuses ?? 0;
|
|
358
|
+
if (statuses > 0) {
|
|
359
|
+
notes.push(
|
|
360
|
+
`${statuses} story ${statuses === 1 ? "status was" : "statuses were"} not applied: intentdocs sets status itself. New stories start as To do, existing ones keep theirs, and they move through build and acceptance in intentdocs.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
const missing = result.summary?.stories?.missing ?? [];
|
|
364
|
+
if (missing.length) notes.push(`In intentdocs but not in intent/: ${missing.join(", ")}. Push never deletes, so they stay.`);
|
|
365
|
+
if (result.not_stored?.length) notes.push(`Not stored by intentdocs yet: ${result.not_stored.join(", ")}. They stay in intent/.`);
|
|
366
|
+
return notes;
|
|
367
|
+
}
|
|
368
|
+
async function runPush(deps) {
|
|
369
|
+
const read = readIntentFolder(deps.cwd);
|
|
370
|
+
if (!read.ok) throw new PushError(`intent/ can't be pushed yet (${read.dir}):`, read.errors);
|
|
371
|
+
const payload = read.payload;
|
|
372
|
+
let auth = deps.getAuth();
|
|
373
|
+
if (!auth) {
|
|
374
|
+
deps.log("You need to log in first. Opening the browser\u2026");
|
|
375
|
+
await deps.login();
|
|
376
|
+
auth = deps.getAuth();
|
|
377
|
+
if (!auth) throw new PushError("Login didn't complete, so nothing was pushed.");
|
|
378
|
+
}
|
|
379
|
+
const existing = deps.readConfig();
|
|
380
|
+
const linked = existing?.projectId && existing.mapId ? { projectId: existing.projectId, mapId: existing.mapId } : null;
|
|
381
|
+
if (existing) {
|
|
382
|
+
const mismatch = checkoutMismatch(existing, auth.binding);
|
|
383
|
+
if (mismatch) throw new PushError(mismatch);
|
|
384
|
+
}
|
|
385
|
+
deps.log(`Pushing "${payload.project.name}": ${summary(payload)}`);
|
|
386
|
+
let workspace = null;
|
|
387
|
+
let result;
|
|
388
|
+
let created = !linked;
|
|
389
|
+
if (linked) {
|
|
390
|
+
result = await deps.api.updateProject(linked.projectId, linked.mapId, payload);
|
|
391
|
+
} else {
|
|
392
|
+
workspace = await pickWorkspace(deps, auth);
|
|
393
|
+
try {
|
|
394
|
+
result = await deps.api.createProject(workspace.id, payload);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
if (!(err instanceof PushApiError && err.code === "ALREADY_PUSHED" && err.projectId)) throw err;
|
|
397
|
+
const [map] = await deps.api.projectMaps(err.projectId);
|
|
398
|
+
if (!map) throw err;
|
|
399
|
+
deps.log(`"${payload.project.name}" was pushed before, so this updates that project.`);
|
|
400
|
+
result = await deps.api.updateProject(err.projectId, map.id, payload);
|
|
401
|
+
created = false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
deps.writeConfig({
|
|
405
|
+
...existing,
|
|
406
|
+
...checkoutConfig({ apiBase: auth.apiBase, project: result.project, map: result.map, appUrl: existing?.appUrl }),
|
|
407
|
+
lastPushed: deps.now().toISOString()
|
|
408
|
+
});
|
|
409
|
+
deps.openBrowser(result.url);
|
|
410
|
+
return { result, created, workspace };
|
|
411
|
+
}
|
|
412
|
+
function openBrowser(url) {
|
|
413
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
414
|
+
exec(cmd);
|
|
415
|
+
}
|
|
416
|
+
async function push() {
|
|
417
|
+
const deps = {
|
|
418
|
+
cwd: process.cwd(),
|
|
419
|
+
api: pushApi(),
|
|
420
|
+
getAuth,
|
|
421
|
+
login,
|
|
422
|
+
readConfig,
|
|
423
|
+
writeConfig,
|
|
424
|
+
openBrowser,
|
|
425
|
+
chooseWorkspace: async (workspaces, defaultIndex) => {
|
|
426
|
+
if (!isInteractive()) return defaultIndex;
|
|
427
|
+
const order = [defaultIndex, ...workspaces.map((_, i) => i).filter((i) => i !== defaultIndex)];
|
|
428
|
+
const labels = order.map((i, n) => `${workspaces[i].personal ? "Personal" : workspaces[i].name ?? "Workspace"}${n === 0 ? " (default)" : ""}`);
|
|
429
|
+
return order[await choose("Which workspace should this project go in?", labels)];
|
|
430
|
+
},
|
|
431
|
+
log: (line) => console.log(line),
|
|
432
|
+
now: () => /* @__PURE__ */ new Date()
|
|
433
|
+
};
|
|
434
|
+
try {
|
|
435
|
+
const { result, created } = await runPush(deps);
|
|
436
|
+
console.log(`
|
|
437
|
+
\u2713 ${created ? "Created" : "Updated"} "${result.map.title}"${result.project.workspace ? ` in ${result.project.workspace.name ?? "your workspace"}` : ""}`);
|
|
438
|
+
for (const note of pushNotes(result)) console.log(` ${note}`);
|
|
439
|
+
console.log(` Linked this repo in .intent/config.json \u2014 run \`npx intentdocs push\` again to sync.`);
|
|
440
|
+
console.log(` Opening ${result.url}
|
|
441
|
+
`);
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (err instanceof PushError) {
|
|
444
|
+
console.error(`\u2717 ${err.message}`);
|
|
445
|
+
for (const d of err.details) console.error(` - ${d}`);
|
|
446
|
+
} else if (err instanceof PushApiError) {
|
|
447
|
+
console.error(`\u2717 ${err.message}`);
|
|
448
|
+
for (const p of err.problems) console.error(` - ${p}`);
|
|
449
|
+
} else {
|
|
450
|
+
console.error(`\u2717 Push failed: ${err.message}`);
|
|
451
|
+
}
|
|
452
|
+
process.exitCode = 1;
|
|
453
|
+
} finally {
|
|
454
|
+
closePrompt();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
export {
|
|
458
|
+
PushError,
|
|
459
|
+
push,
|
|
460
|
+
pushNotes,
|
|
461
|
+
runPush
|
|
462
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
connectCheckout,
|
|
3
|
+
scope
|
|
4
|
+
} from "./chunk-ID4B5RZ2.js";
|
|
5
|
+
import "./chunk-KTGQO3AE.js";
|
|
6
|
+
import "./chunk-K5SNF6CG.js";
|
|
7
|
+
import "./chunk-WRRUCCVZ.js";
|
|
8
|
+
import "./chunk-XZE2XP6O.js";
|
|
9
|
+
import "./chunk-UYTVTYNB.js";
|
|
10
|
+
export {
|
|
11
|
+
connectCheckout,
|
|
12
|
+
scope
|
|
13
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
setup
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-F52HPVNQ.js";
|
|
3
|
+
} from "./chunk-IORMKLAQ.js";
|
|
4
|
+
import "./chunk-BNEU6TLJ.js";
|
|
6
5
|
import "./chunk-LQTPZL2Y.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
import "./chunk-KPQ42YE6.js";
|
|
7
|
+
import "./chunk-KTGQO3AE.js";
|
|
8
|
+
import "./chunk-K5SNF6CG.js";
|
|
9
9
|
import "./chunk-WRRUCCVZ.js";
|
|
10
10
|
import "./chunk-XZE2XP6O.js";
|
|
11
11
|
import "./chunk-UYTVTYNB.js";
|