intentdocs 0.8.0 → 0.10.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 +2 -1
- package/bin/intentdocs.js +2 -0
- package/dist/{chunk-4OWPN2RE.js → chunk-5USVG4DW.js} +24 -43
- package/dist/{chunk-IKF3ZLHK.js → chunk-EEKEF2FD.js} +65 -101
- package/dist/chunk-HEHHSSUM.js +43 -0
- package/dist/chunk-IDNUBI2P.js +317 -0
- package/dist/chunk-LQTPZL2Y.js +15 -0
- package/dist/chunk-LXQXJULN.js +105 -0
- package/dist/{chunk-6FGYJ3W2.js → chunk-NFIXRIHM.js} +43 -4
- package/dist/{chunk-ICOMMHKG.js → chunk-UF535HZL.js} +12 -1
- package/dist/{chunk-ITQUQYWB.js → chunk-UR73WX5I.js} +1 -1
- package/dist/chunk-UYTVTYNB.js +11 -0
- package/dist/{chunk-WPVDURTJ.js → chunk-WRRUCCVZ.js} +27 -3
- package/dist/chunk-YUW3W43S.js +170 -0
- package/dist/ci-LBOCMRKZ.js +11 -0
- package/dist/{complexity-NSSYVT54.js → complexity-ZOB3BPVD.js} +12 -7
- package/dist/{import-prd-CKV3R67G.js → import-prd-MJUCIMHX.js} +13 -11
- package/dist/index.js +420 -179
- package/dist/{keys-EDWET3WG.js → keys-5PA3D6CX.js} +8 -7
- package/dist/log-EVLWM64C.js +206 -0
- package/dist/{map-extract-I3AACTGL.js → map-extract-W6WJLVOF.js} +2 -1
- package/dist/{map-import-IYUMRASO.js → map-import-LL4AIAQE.js} +12 -10
- package/dist/{map-schema-JVL3O2TJ.js → map-schema-6GL2UKZQ.js} +1 -1
- package/dist/projects-F7GMH66U.js +37 -0
- package/dist/{prompt-WAWCRGHN.js → prompt-GOAFIKBZ.js} +3 -1
- package/dist/{research-6SXJ37TI.js → research-4ASCZSZN.js} +12 -10
- package/dist/scope-S3LZZ3KB.js +11 -0
- package/dist/setup-YLUPBLK6.js +14 -0
- package/package.json +4 -2
- package/dist/chunk-6XKB6RTU.js +0 -123
- package/dist/chunk-PCNDP5SX.js +0 -34
- package/dist/log-TT2UCJP7.js +0 -94
- package/dist/scope-OGKQ472D.js +0 -10
- package/dist/setup-SOI2HQAL.js +0 -11
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
captureCli,
|
|
3
|
+
shutdownCliAnalytics
|
|
4
|
+
} from "./chunk-HEHHSSUM.js";
|
|
5
|
+
import {
|
|
6
|
+
requireConfig
|
|
7
|
+
} from "./chunk-UF535HZL.js";
|
|
8
|
+
import {
|
|
9
|
+
closePrompt,
|
|
10
|
+
confirm,
|
|
11
|
+
isInteractive
|
|
12
|
+
} from "./chunk-WRRUCCVZ.js";
|
|
13
|
+
import {
|
|
14
|
+
authenticatedFetch,
|
|
15
|
+
requireValidAuth
|
|
16
|
+
} from "./chunk-LXQXJULN.js";
|
|
17
|
+
import {
|
|
18
|
+
canonicalApiBase
|
|
19
|
+
} from "./chunk-UYTVTYNB.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/ci.ts
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
|
|
24
|
+
import { join as join2 } from "path";
|
|
25
|
+
|
|
26
|
+
// src/lib/ci-workflow.ts
|
|
27
|
+
import { existsSync, readFileSync } from "fs";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
var CI_SECRET_NAME = "INTENTDOCS_CI_KEY";
|
|
30
|
+
function detectTestRunner(cwd) {
|
|
31
|
+
try {
|
|
32
|
+
const pkgPath = join(cwd, "package.json");
|
|
33
|
+
if (existsSync(pkgPath)) {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
36
|
+
if (deps["vitest"]) return "vitest";
|
|
37
|
+
if (deps["jest"]) return "jest";
|
|
38
|
+
}
|
|
39
|
+
if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "pytest.ini")) || existsSync(join(cwd, "requirements.txt"))) {
|
|
40
|
+
return "pytest";
|
|
41
|
+
}
|
|
42
|
+
if (existsSync(join(cwd, "go.mod"))) return "go";
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
var JUNIT_ARTIFACT = "test-results.xml";
|
|
48
|
+
function junitTestCommand(runner) {
|
|
49
|
+
switch (runner) {
|
|
50
|
+
case "vitest":
|
|
51
|
+
return `npx vitest run --reporter=junit --outputFile=${JUNIT_ARTIFACT}`;
|
|
52
|
+
case "jest":
|
|
53
|
+
return `JEST_JUNIT_OUTPUT_NAME=${JUNIT_ARTIFACT} npx jest --ci --reporters=default --reporters=jest-junit`;
|
|
54
|
+
case "pytest":
|
|
55
|
+
return `pytest --junitxml=${JUNIT_ARTIFACT}`;
|
|
56
|
+
case "go":
|
|
57
|
+
return `go test ./... -v 2>&1 | go-junit-report > ${JUNIT_ARTIFACT}`;
|
|
58
|
+
case "unknown":
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function reportCurl(t, indent) {
|
|
63
|
+
const secret = t.secretName ?? CI_SECRET_NAME;
|
|
64
|
+
const base = canonicalApiBase(t.apiBase);
|
|
65
|
+
return [
|
|
66
|
+
`curl -sS -X POST "${base}/api/ci/${t.projectId}/report" \\`,
|
|
67
|
+
`${indent} -H "Authorization: Bearer $${secret}" \\`,
|
|
68
|
+
`${indent} -H "X-Commit-SHA: $COMMIT_SHA" \\`,
|
|
69
|
+
`${indent} -H "X-Branch: $BRANCH" \\`,
|
|
70
|
+
`${indent} -H "Content-Type: application/xml" \\`,
|
|
71
|
+
`${indent} --data-binary @${JUNIT_ARTIFACT}`
|
|
72
|
+
].join("\n");
|
|
73
|
+
}
|
|
74
|
+
function reportStepYaml(t) {
|
|
75
|
+
const secret = t.secretName ?? CI_SECRET_NAME;
|
|
76
|
+
const base = canonicalApiBase(t.apiBase);
|
|
77
|
+
return [
|
|
78
|
+
` - name: Report test results to IntentDocs`,
|
|
79
|
+
` if: always()`,
|
|
80
|
+
` env:`,
|
|
81
|
+
` ${secret}: \${{ secrets.${secret} }}`,
|
|
82
|
+
` COMMIT_SHA: \${{ github.sha }}`,
|
|
83
|
+
` BRANCH: \${{ github.ref_name }}`,
|
|
84
|
+
` run: |`,
|
|
85
|
+
` curl -sS -X POST "${base}/api/ci/${t.projectId}/report" \\`,
|
|
86
|
+
` -H "Authorization: Bearer $${secret}" \\`,
|
|
87
|
+
` -H "X-Commit-SHA: $COMMIT_SHA" \\`,
|
|
88
|
+
` -H "X-Branch: $BRANCH" \\`,
|
|
89
|
+
` -H "Content-Type: application/xml" \\`,
|
|
90
|
+
` --data-binary @${JUNIT_ARTIFACT}`
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
function standaloneWorkflowYaml(t) {
|
|
94
|
+
const testCmd = junitTestCommand(t.runner);
|
|
95
|
+
if (!testCmd) return null;
|
|
96
|
+
const isNode = t.runner === "vitest" || t.runner === "jest";
|
|
97
|
+
const setup = isNode ? [
|
|
98
|
+
` - uses: actions/setup-node@v4`,
|
|
99
|
+
` with:`,
|
|
100
|
+
` node-version: 20`,
|
|
101
|
+
` - run: npm ci`
|
|
102
|
+
] : t.runner === "pytest" ? [
|
|
103
|
+
` - uses: actions/setup-python@v5`,
|
|
104
|
+
` with:`,
|
|
105
|
+
` python-version: "3.12"`,
|
|
106
|
+
` - run: pip install -r requirements.txt`
|
|
107
|
+
] : [
|
|
108
|
+
` - uses: actions/setup-go@v5`,
|
|
109
|
+
` with:`,
|
|
110
|
+
` go-version: stable`,
|
|
111
|
+
` - run: go install github.com/jstemmer/go-junit-report/v2@latest`
|
|
112
|
+
];
|
|
113
|
+
return [
|
|
114
|
+
`# Generated by \`intentdocs ci\`. Reports each test's result to its story.`,
|
|
115
|
+
`name: Tests`,
|
|
116
|
+
`on:`,
|
|
117
|
+
` push:`,
|
|
118
|
+
` pull_request:`,
|
|
119
|
+
`jobs:`,
|
|
120
|
+
` test:`,
|
|
121
|
+
` runs-on: ubuntu-latest`,
|
|
122
|
+
` steps:`,
|
|
123
|
+
` - uses: actions/checkout@v4`,
|
|
124
|
+
...setup,
|
|
125
|
+
` - name: Run tests`,
|
|
126
|
+
` run: ${testCmd}`,
|
|
127
|
+
reportStepYaml(t),
|
|
128
|
+
``
|
|
129
|
+
].join("\n");
|
|
130
|
+
}
|
|
131
|
+
function reportCommandSnippet(t) {
|
|
132
|
+
const secret = t.secretName ?? CI_SECRET_NAME;
|
|
133
|
+
return [
|
|
134
|
+
`# After your test step writes ${JUNIT_ARTIFACT}, with ${secret} in the environment:`,
|
|
135
|
+
`COMMIT_SHA=<your commit sha> BRANCH=<your branch> \\`,
|
|
136
|
+
` ${reportCurl(t, " ")}`
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/commands/ci.ts
|
|
141
|
+
function ghReady() {
|
|
142
|
+
try {
|
|
143
|
+
execSync("gh auth status", { stdio: "ignore" });
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isGitHubRepo(cwd) {
|
|
150
|
+
if (existsSync2(join2(cwd, ".github", "workflows"))) return true;
|
|
151
|
+
try {
|
|
152
|
+
const remote = execSync("git remote get-url origin", { cwd, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
153
|
+
return remote.includes("github.com");
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function timeAgo(iso) {
|
|
159
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
160
|
+
const mins = Math.round(diff / 6e4);
|
|
161
|
+
if (mins < 1) return "just now";
|
|
162
|
+
if (mins < 60) return `${mins}m ago`;
|
|
163
|
+
const hrs = Math.round(mins / 60);
|
|
164
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
165
|
+
return `${Math.round(hrs / 24)}d ago`;
|
|
166
|
+
}
|
|
167
|
+
async function listKeys(projectId) {
|
|
168
|
+
const res = await authenticatedFetch(`/api/cli/projects/${projectId}/ci-keys`);
|
|
169
|
+
if (!res.ok) return [];
|
|
170
|
+
const data = await res.json();
|
|
171
|
+
return data.keys ?? [];
|
|
172
|
+
}
|
|
173
|
+
async function reportStatus(projectId) {
|
|
174
|
+
const keys = (await listKeys(projectId)).filter((k) => !k.revoked_at);
|
|
175
|
+
if (keys.length === 0) {
|
|
176
|
+
console.log("No CI key yet. Run `intentdocs ci` to set up test reporting.");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const reported = keys.filter((k) => k.last_report_at);
|
|
180
|
+
if (reported.length === 0) {
|
|
181
|
+
console.log("A CI key is set up, but no results have arrived yet. They appear here after your next CI run on the default branch.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const k of reported) {
|
|
185
|
+
const total = k.last_report_total ?? 0;
|
|
186
|
+
const unmatched = k.last_report_unmatched ?? 0;
|
|
187
|
+
const matched = Math.max(0, total - unmatched);
|
|
188
|
+
console.log(
|
|
189
|
+
`Last report ${timeAgo(k.last_report_at)}: ${matched} of ${total} result${total === 1 ? "" : "s"} matched a saved test` + (unmatched > 0 ? `, ${unmatched} did not.` : ".")
|
|
190
|
+
);
|
|
191
|
+
if (unmatched > 0) {
|
|
192
|
+
console.log(" Unmatched results are tests CI ran that no story records. Save them with file + full name so they reach a story.");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async function ci(opts = {}) {
|
|
197
|
+
const config = requireConfig();
|
|
198
|
+
await requireValidAuth();
|
|
199
|
+
const projectId = config.projectId;
|
|
200
|
+
if (!projectId) {
|
|
201
|
+
console.error("This checkout has no project id. Run `intentdocs init` to reconnect.");
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
if (opts.status) {
|
|
205
|
+
try {
|
|
206
|
+
await reportStatus(projectId);
|
|
207
|
+
} finally {
|
|
208
|
+
await shutdownCliAnalytics();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const cwd = process.cwd();
|
|
213
|
+
const start = Date.now();
|
|
214
|
+
try {
|
|
215
|
+
const existing = (await listKeys(projectId)).filter((k) => !k.revoked_at);
|
|
216
|
+
let rawKey = null;
|
|
217
|
+
if (existing.length > 0 && !opts.new) {
|
|
218
|
+
console.log(`
|
|
219
|
+
An active CI key already exists (${existing[0].key_prefix}\u2026). Its value can't be shown again.`);
|
|
220
|
+
if (isInteractive()) {
|
|
221
|
+
const rotate = await confirm("Create a new key and rotate it?");
|
|
222
|
+
if (rotate) rawKey = await createKey(projectId);
|
|
223
|
+
} else {
|
|
224
|
+
console.log("Re-run with --new to rotate it. Continuing with workflow setup \u2014 make sure the repo secret is already set.\n");
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
rawKey = await createKey(projectId);
|
|
228
|
+
}
|
|
229
|
+
const runner = detectTestRunner(cwd);
|
|
230
|
+
const base = config.apiBase;
|
|
231
|
+
const onGitHub = isGitHubRepo(cwd);
|
|
232
|
+
if (onGitHub) {
|
|
233
|
+
const workflow = standaloneWorkflowYaml({ runner, apiBase: base, projectId });
|
|
234
|
+
const workflowPath = join2(cwd, ".github", "workflows", "intentdocs.yml");
|
|
235
|
+
if (workflow && !existsSync2(workflowPath)) {
|
|
236
|
+
mkdirSync(join2(cwd, ".github", "workflows"), { recursive: true });
|
|
237
|
+
writeFileSync(workflowPath, workflow);
|
|
238
|
+
console.log(`
|
|
239
|
+
\u2713 Wrote .github/workflows/intentdocs.yml \u2014 it runs your tests and reports each result to its story.`);
|
|
240
|
+
} else if (existsSync2(workflowPath)) {
|
|
241
|
+
console.log(`
|
|
242
|
+
.github/workflows/intentdocs.yml already exists \u2014 leaving it as is.`);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(`
|
|
245
|
+
Add this step to the GitHub Actions workflow that runs your tests, after the test step:
|
|
246
|
+
`);
|
|
247
|
+
console.log(reportStepYaml({ apiBase: base, projectId }));
|
|
248
|
+
console.log(`
|
|
249
|
+
Your test step must write JUnit XML to test-results.xml first.`);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
console.log(`
|
|
253
|
+
No GitHub repo detected. Add this to your CI after your tests write JUnit XML:
|
|
254
|
+
`);
|
|
255
|
+
console.log(reportCommandSnippet({ apiBase: base, projectId }));
|
|
256
|
+
}
|
|
257
|
+
if (rawKey) {
|
|
258
|
+
if (onGitHub && ghReady()) {
|
|
259
|
+
try {
|
|
260
|
+
execSync(`gh secret set ${CI_SECRET_NAME}`, { input: rawKey, stdio: ["pipe", "ignore", "ignore"] });
|
|
261
|
+
console.log(`
|
|
262
|
+
\u2713 Stored the key as the ${CI_SECRET_NAME} repo secret with gh.`);
|
|
263
|
+
} catch {
|
|
264
|
+
printSecretInstructions(rawKey);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
printSecretInstructions(rawKey);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const testCmd = junitTestCommand(runner);
|
|
271
|
+
if (testCmd) {
|
|
272
|
+
console.log(`
|
|
273
|
+
Tell your agent to save each test with its file and full name so CI results can reach the story \u2014 see the "Set up test reporting" prompt in the Prompt library.`);
|
|
274
|
+
}
|
|
275
|
+
captureCli(
|
|
276
|
+
"ci_setup",
|
|
277
|
+
{ runner, on_github: onGitHub, created_key: Boolean(rawKey), duration_ms: Date.now() - start },
|
|
278
|
+
{ project: projectId }
|
|
279
|
+
);
|
|
280
|
+
console.log(`
|
|
281
|
+
After your next default-branch CI run, check how results matched with: intentdocs ci --status
|
|
282
|
+
`);
|
|
283
|
+
} finally {
|
|
284
|
+
closePrompt();
|
|
285
|
+
await shutdownCliAnalytics();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function createKey(projectId) {
|
|
289
|
+
const res = await authenticatedFetch(`/api/cli/projects/${projectId}/ci-keys`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
body: JSON.stringify({ label: "intentdocs ci" })
|
|
292
|
+
});
|
|
293
|
+
if (!res.ok) {
|
|
294
|
+
console.error("Could not create a CI key. Only the project owner can \u2014 check you're logged in as the owner.");
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const data = await res.json();
|
|
298
|
+
if (!data.raw_key) {
|
|
299
|
+
console.error("The server did not return a key. Try again.");
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
console.log("\n\u2713 Created a project CI key (shown once below \u2014 store it now).");
|
|
303
|
+
return data.raw_key;
|
|
304
|
+
}
|
|
305
|
+
function printSecretInstructions(rawKey) {
|
|
306
|
+
console.log(`
|
|
307
|
+
Add this as a repo secret named ${CI_SECRET_NAME}:
|
|
308
|
+
`);
|
|
309
|
+
console.log(` ${rawKey}
|
|
310
|
+
`);
|
|
311
|
+
console.log(`GitHub: Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret.`);
|
|
312
|
+
console.log(`Or with the GitHub CLI: gh secret set ${CI_SECRET_NAME}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export {
|
|
316
|
+
ci
|
|
317
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/lib/story-ref.ts
|
|
2
|
+
function storyRef(story) {
|
|
3
|
+
return story.key ?? story.id.slice(0, 8);
|
|
4
|
+
}
|
|
5
|
+
function matchesStoryRef(story, typed) {
|
|
6
|
+
const ref = typed.trim();
|
|
7
|
+
if (!ref) return false;
|
|
8
|
+
if (story.key && story.key.toLowerCase() === ref.toLowerCase()) return true;
|
|
9
|
+
return story.id === ref || story.id.startsWith(ref);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
storyRef,
|
|
14
|
+
matchesStoryRef
|
|
15
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
API_BASE,
|
|
3
|
+
canonicalApiBase
|
|
4
|
+
} from "./chunk-UYTVTYNB.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/auth.ts
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var AUTH_DIR = join(homedir(), ".intent");
|
|
11
|
+
var AUTH_FILE = join(AUTH_DIR, "auth.json");
|
|
12
|
+
function bindingLabel(binding) {
|
|
13
|
+
if (!binding) return "personal";
|
|
14
|
+
const name = binding.workspaceName ?? "a workspace";
|
|
15
|
+
if (binding.approvalStatus === "pending") return `bound to ${name} (awaiting an admin's approval)`;
|
|
16
|
+
if (binding.approvalStatus === "revoked") return `bound to ${name} (access revoked)`;
|
|
17
|
+
return `bound to ${name}`;
|
|
18
|
+
}
|
|
19
|
+
function getAuth() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
22
|
+
const data = JSON.parse(raw);
|
|
23
|
+
return { ...data, apiBase: canonicalApiBase(data.apiBase ?? API_BASE) };
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function saveAuth(data) {
|
|
29
|
+
if (!existsSync(AUTH_DIR)) mkdirSync(AUTH_DIR, { recursive: true });
|
|
30
|
+
writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
|
|
31
|
+
}
|
|
32
|
+
function clearAuth() {
|
|
33
|
+
try {
|
|
34
|
+
if (existsSync(AUTH_FILE)) unlinkSync(AUTH_FILE);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function validateAuth() {
|
|
39
|
+
const auth = getAuth();
|
|
40
|
+
if (!auth) return { valid: false, reason: "no_file" };
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`${auth.apiBase}/api/cli/auth/verify`, {
|
|
43
|
+
headers: { Authorization: `Bearer ${auth.token}` }
|
|
44
|
+
});
|
|
45
|
+
if (res.ok) {
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
if (data.valid) {
|
|
48
|
+
if (data.binding !== void 0 && JSON.stringify(data.binding) !== JSON.stringify(auth.binding ?? null)) {
|
|
49
|
+
auth.binding = data.binding;
|
|
50
|
+
saveAuth(auth);
|
|
51
|
+
}
|
|
52
|
+
return { valid: true, userId: data.userId ?? auth.userId ?? "unknown", auth };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const body = await res.json().catch(() => ({}));
|
|
56
|
+
const reason = body.reason;
|
|
57
|
+
if (reason === "revoked") return { valid: false, reason: "revoked" };
|
|
58
|
+
if (reason === "not_found") return { valid: false, reason: "not_found" };
|
|
59
|
+
return { valid: false, reason: "not_found" };
|
|
60
|
+
} catch {
|
|
61
|
+
return { valid: false, reason: "network_error" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function requireValidAuth() {
|
|
65
|
+
const result = await validateAuth();
|
|
66
|
+
if (result.valid) return result.auth;
|
|
67
|
+
switch (result.reason) {
|
|
68
|
+
case "no_file":
|
|
69
|
+
console.error("Not logged in \u2014 no auth credentials found. Run: intentdocs login");
|
|
70
|
+
break;
|
|
71
|
+
case "revoked":
|
|
72
|
+
console.error("Your auth token has been revoked. Run: intentdocs login");
|
|
73
|
+
clearAuth();
|
|
74
|
+
break;
|
|
75
|
+
case "not_found":
|
|
76
|
+
console.error("Your auth token is no longer valid (it may have been deleted). Run: intentdocs login");
|
|
77
|
+
clearAuth();
|
|
78
|
+
break;
|
|
79
|
+
case "network_error":
|
|
80
|
+
console.error("Could not reach intentdocs.com to verify your auth token. Check your internet connection and try again.");
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
async function authenticatedFetch(path, init) {
|
|
86
|
+
const auth = await requireValidAuth();
|
|
87
|
+
return fetch(`${auth.apiBase}${path}`, {
|
|
88
|
+
...init,
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": "application/json",
|
|
91
|
+
Authorization: `Bearer ${auth.token}`,
|
|
92
|
+
...init?.headers ?? {}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
bindingLabel,
|
|
99
|
+
getAuth,
|
|
100
|
+
saveAuth,
|
|
101
|
+
clearAuth,
|
|
102
|
+
validateAuth,
|
|
103
|
+
requireValidAuth,
|
|
104
|
+
authenticatedFetch
|
|
105
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
storyRef
|
|
3
|
+
} from "./chunk-LQTPZL2Y.js";
|
|
4
|
+
|
|
1
5
|
// src/lib/story-context.ts
|
|
2
6
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "fs";
|
|
3
7
|
import { join } from "path";
|
|
@@ -6,9 +10,12 @@ import { join } from "path";
|
|
|
6
10
|
function criterionPriority(ac) {
|
|
7
11
|
return ac.priority ?? "should";
|
|
8
12
|
}
|
|
13
|
+
function isCriterionCancelled(ac) {
|
|
14
|
+
return !!ac.cancelled_at;
|
|
15
|
+
}
|
|
9
16
|
function findBlockingMusts(criteria) {
|
|
10
17
|
return criteria.filter(
|
|
11
|
-
(ac) => criterionPriority(ac) === "must" && ac.last_result?.status !== "pass"
|
|
18
|
+
(ac) => !isCriterionCancelled(ac) && criterionPriority(ac) === "must" && ac.last_result?.status !== "pass"
|
|
12
19
|
);
|
|
13
20
|
}
|
|
14
21
|
function criterionStatusLabel(ac) {
|
|
@@ -22,6 +29,12 @@ var METHOD_LABELS = {
|
|
|
22
29
|
};
|
|
23
30
|
function renderCriterionLines(ac) {
|
|
24
31
|
const checkbox = ac.done ? "x" : " ";
|
|
32
|
+
if (isCriterionCancelled(ac)) {
|
|
33
|
+
const reason = ac.cancellation_reason?.trim();
|
|
34
|
+
return [
|
|
35
|
+
`- [-] ~~${ac.text}~~ (cancelled${reason ? `: ${reason}` : ""})`
|
|
36
|
+
];
|
|
37
|
+
}
|
|
25
38
|
const isLegacy = ac.priority === void 0 && ac.verification === void 0;
|
|
26
39
|
if (isLegacy) {
|
|
27
40
|
return [`- [${checkbox}] ${ac.text}`];
|
|
@@ -39,6 +52,28 @@ function renderCriterionLines(ac) {
|
|
|
39
52
|
return lines;
|
|
40
53
|
}
|
|
41
54
|
|
|
55
|
+
// src/lib/design-refs.ts
|
|
56
|
+
var MATCH_INSTRUCTION = {
|
|
57
|
+
figma: "Match this Figma frame/node exactly \u2014 fetch it with get_design if you need the visual, or open the link.",
|
|
58
|
+
"claude-design": "Match this design exactly \u2014 fetch it with get_design for the rendered HTML, or open the link.",
|
|
59
|
+
"image-url": "Match this reference image exactly.",
|
|
60
|
+
"repo-path": "Match the layout and patterns already in this file/folder.",
|
|
61
|
+
storybook: "Match this Storybook story's states and props.",
|
|
62
|
+
other: "Match this reference."
|
|
63
|
+
};
|
|
64
|
+
function designRefMatchInstruction(ref) {
|
|
65
|
+
return MATCH_INSTRUCTION[ref.kind];
|
|
66
|
+
}
|
|
67
|
+
function designRefHasDrifted(ref) {
|
|
68
|
+
return !!ref.baseline_hash && !!ref.content_hash && ref.baseline_hash !== ref.content_hash;
|
|
69
|
+
}
|
|
70
|
+
function formatDesignRefLine(ref) {
|
|
71
|
+
const parts = [`[${ref.kind}]`, ref.url];
|
|
72
|
+
if (ref.selector) parts.push(`(${ref.selector})`);
|
|
73
|
+
const drift = designRefHasDrifted(ref) ? " (design changed since this story was written)" : "";
|
|
74
|
+
return `${parts.join(" ")} \u2014 ${designRefMatchInstruction(ref)}${drift}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
// src/lib/story-context.ts
|
|
43
78
|
var CONTEXT_DIR = ".intent/stories";
|
|
44
79
|
function storyContextPath(storyId) {
|
|
@@ -49,6 +84,8 @@ function writeStoryContext(story) {
|
|
|
49
84
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
50
85
|
const acLines = story.acceptance_criteria.length ? story.acceptance_criteria.flatMap((ac) => renderCriterionLines(ac)).join("\n") : "_No acceptance criteria defined._";
|
|
51
86
|
const specSection = story.notes?.trim() ? [`## Spec`, ``, story.notes.trim(), ``] : [];
|
|
87
|
+
const designRefs = story.design_refs ?? [];
|
|
88
|
+
const designRefsSection = designRefs.length ? [`## Design references`, ``, ...designRefs.map((ref) => `- ${formatDesignRefLine(ref)}`), ``] : [];
|
|
52
89
|
const plan = story.implementation_plan;
|
|
53
90
|
const hasPlan = plan?.status === "ready" && plan.markdown;
|
|
54
91
|
const approachSection = hasPlan ? [
|
|
@@ -82,15 +119,16 @@ function writeStoryContext(story) {
|
|
|
82
119
|
``,
|
|
83
120
|
acLines,
|
|
84
121
|
``,
|
|
122
|
+
...designRefsSection,
|
|
85
123
|
...specSection,
|
|
86
124
|
...approachSection,
|
|
87
125
|
`## Implementation notes`,
|
|
88
126
|
``,
|
|
89
|
-
`_Notes added via \`
|
|
127
|
+
`_Notes added via \`intentdocs note ${storyRef(story)} "..."\`_`,
|
|
90
128
|
``,
|
|
91
129
|
`## Decisions`,
|
|
92
130
|
``,
|
|
93
|
-
`_Log decisions via \`
|
|
131
|
+
`_Log decisions via \`intentdocs decide "..." -s ${storyRef(story)}\`_`,
|
|
94
132
|
``,
|
|
95
133
|
`## Relevant files`,
|
|
96
134
|
``,
|
|
@@ -98,7 +136,7 @@ function writeStoryContext(story) {
|
|
|
98
136
|
``,
|
|
99
137
|
`---`,
|
|
100
138
|
``,
|
|
101
|
-
`Mark done: \`
|
|
139
|
+
`Mark done: \`intentdocs done ${storyRef(story)}\``
|
|
102
140
|
].join("\n");
|
|
103
141
|
const path = storyContextPath(story.id);
|
|
104
142
|
writeFileSync(path, md);
|
|
@@ -134,6 +172,7 @@ export {
|
|
|
134
172
|
criterionPriority,
|
|
135
173
|
findBlockingMusts,
|
|
136
174
|
criterionStatusLabel,
|
|
175
|
+
formatDesignRefLine,
|
|
137
176
|
storyContextPath,
|
|
138
177
|
writeStoryContext,
|
|
139
178
|
appendNoteToContext
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalApiBase
|
|
3
|
+
} from "./chunk-UYTVTYNB.js";
|
|
4
|
+
|
|
1
5
|
// src/lib/config.ts
|
|
2
6
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
7
|
import { join, dirname } from "path";
|
|
@@ -20,7 +24,8 @@ function hasConfig() {
|
|
|
20
24
|
function readConfig() {
|
|
21
25
|
try {
|
|
22
26
|
const raw = readFileSync(configPath(), "utf-8");
|
|
23
|
-
|
|
27
|
+
const config = JSON.parse(raw);
|
|
28
|
+
return config.apiBase ? { ...config, apiBase: canonicalApiBase(config.apiBase) } : config;
|
|
24
29
|
} catch {
|
|
25
30
|
return null;
|
|
26
31
|
}
|
|
@@ -37,6 +42,12 @@ function requireConfig() {
|
|
|
37
42
|
console.error("Not initialized. Run: intent init");
|
|
38
43
|
process.exit(1);
|
|
39
44
|
}
|
|
45
|
+
if (!config.mapId) {
|
|
46
|
+
console.error(
|
|
47
|
+
"This checkout was connected with a sync token, which has been retired: every sync now runs as you.\nRemove .intent/config.json and run `intentdocs login` then `intentdocs init` to reconnect."
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
40
51
|
return config;
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -10,7 +10,7 @@ function printUpgradeMessage() {
|
|
|
10
10
|
` ${chalk.yellow("This feature requires a Pro plan or your own Anthropic key.")}`
|
|
11
11
|
);
|
|
12
12
|
console.log(` Upgrade: ${chalk.cyan.underline("https://intentdocs.com/pricing")}`);
|
|
13
|
-
console.log(` Or add your key: ${chalk.cyan("
|
|
13
|
+
console.log(` Or add your key: ${chalk.cyan("intentdocs keys set")}`);
|
|
14
14
|
console.log();
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/lib/api-base.ts
|
|
2
|
+
var API_BASE = "https://www.intentdocs.com";
|
|
3
|
+
function canonicalApiBase(base) {
|
|
4
|
+
const trimmed = base.trim().replace(/\/+$/, "");
|
|
5
|
+
return /^https?:\/\/intentdocs\.com$/i.test(trimmed) ? API_BASE : trimmed;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
API_BASE,
|
|
10
|
+
canonicalApiBase
|
|
11
|
+
};
|
|
@@ -1,18 +1,41 @@
|
|
|
1
1
|
// src/lib/prompt.ts
|
|
2
2
|
import { createInterface } from "readline";
|
|
3
3
|
var rl = null;
|
|
4
|
+
function stdinExhausted() {
|
|
5
|
+
return process.stdin.readableEnded === true || process.stdin.destroyed === true;
|
|
6
|
+
}
|
|
7
|
+
function isInteractive() {
|
|
8
|
+
return process.stdin.isTTY === true;
|
|
9
|
+
}
|
|
4
10
|
function getRL() {
|
|
5
11
|
if (!rl) {
|
|
6
|
-
|
|
12
|
+
const iface = createInterface({
|
|
7
13
|
input: process.stdin,
|
|
8
14
|
output: process.stdout
|
|
9
15
|
});
|
|
16
|
+
iface.on("close", () => {
|
|
17
|
+
if (rl === iface) rl = null;
|
|
18
|
+
});
|
|
19
|
+
rl = iface;
|
|
10
20
|
}
|
|
11
21
|
return rl;
|
|
12
22
|
}
|
|
13
23
|
function ask(question) {
|
|
24
|
+
if (stdinExhausted()) return Promise.resolve("");
|
|
14
25
|
return new Promise((resolve) => {
|
|
15
|
-
getRL()
|
|
26
|
+
const iface = getRL();
|
|
27
|
+
let settled = false;
|
|
28
|
+
const onClose = () => {
|
|
29
|
+
if (!settled) {
|
|
30
|
+
settled = true;
|
|
31
|
+
resolve("");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
iface.once("close", onClose);
|
|
35
|
+
iface.question(question, (answer) => {
|
|
36
|
+
if (settled) return;
|
|
37
|
+
settled = true;
|
|
38
|
+
iface.removeListener("close", onClose);
|
|
16
39
|
resolve(answer.trim());
|
|
17
40
|
});
|
|
18
41
|
});
|
|
@@ -53,7 +76,7 @@ ${question}
|
|
|
53
76
|
const answer = await ask(`
|
|
54
77
|
Choice (1-${options.length}): `);
|
|
55
78
|
const idx = parseInt(answer, 10) - 1;
|
|
56
|
-
if (idx < 0 || idx >= options.length) return 0;
|
|
79
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= options.length) return 0;
|
|
57
80
|
return idx;
|
|
58
81
|
}
|
|
59
82
|
function closePrompt() {
|
|
@@ -70,6 +93,7 @@ function pausePrompt() {
|
|
|
70
93
|
}
|
|
71
94
|
|
|
72
95
|
export {
|
|
96
|
+
isInteractive,
|
|
73
97
|
ask,
|
|
74
98
|
askMultiline,
|
|
75
99
|
confirm,
|