intentdocs 0.8.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +7 -1
  2. package/bin/intentdocs.js +2 -0
  3. package/dist/chunk-34JCN66H.js +170 -0
  4. package/dist/chunk-6X4BORMW.js +317 -0
  5. package/dist/{chunk-6FGYJ3W2.js → chunk-F52HPVNQ.js} +43 -4
  6. package/dist/{chunk-ICOMMHKG.js → chunk-JX2RLGOM.js} +13 -2
  7. package/dist/chunk-KPQ42YE6.js +43 -0
  8. package/dist/chunk-LQTPZL2Y.js +15 -0
  9. package/dist/{chunk-4OWPN2RE.js → chunk-NHNBCNND.js} +28 -47
  10. package/dist/{chunk-IKF3ZLHK.js → chunk-PUGYRSMK.js} +72 -108
  11. package/dist/{chunk-ITQUQYWB.js → chunk-UQDMUKLP.js} +2 -2
  12. package/dist/chunk-UYTVTYNB.js +11 -0
  13. package/dist/{chunk-WPVDURTJ.js → chunk-WRRUCCVZ.js} +27 -3
  14. package/dist/chunk-XZE2XP6O.js +105 -0
  15. package/dist/ci-XCOXFTLB.js +11 -0
  16. package/dist/{complexity-NSSYVT54.js → complexity-X3IJKNNV.js} +12 -7
  17. package/dist/{import-prd-CKV3R67G.js → import-prd-IJTM6FZE.js} +13 -11
  18. package/dist/index.js +435 -194
  19. package/dist/{keys-EDWET3WG.js → keys-UVXCCWCS.js} +8 -7
  20. package/dist/log-XIH4BGFL.js +206 -0
  21. package/dist/{map-extract-I3AACTGL.js → map-extract-V6D27I3H.js} +3 -2
  22. package/dist/{map-import-IYUMRASO.js → map-import-LORTI4K4.js} +12 -10
  23. package/dist/{map-schema-JVL3O2TJ.js → map-schema-5BEIJM2J.js} +1 -1
  24. package/dist/projects-AYAYVFKL.js +37 -0
  25. package/dist/{prompt-WAWCRGHN.js → prompt-GOAFIKBZ.js} +3 -1
  26. package/dist/{research-6SXJ37TI.js → research-CO3W7PLC.js} +12 -10
  27. package/dist/scope-ZZLKQEF5.js +11 -0
  28. package/dist/setup-WSYIG2CR.js +14 -0
  29. package/package.json +4 -2
  30. package/dist/chunk-6XKB6RTU.js +0 -123
  31. package/dist/chunk-PCNDP5SX.js +0 -34
  32. package/dist/log-TT2UCJP7.js +0 -94
  33. package/dist/scope-OGKQ472D.js +0 -10
  34. package/dist/setup-SOI2HQAL.js +0 -11
package/README.md CHANGED
@@ -98,6 +98,11 @@ intent sync # Refresh INTENT.md
98
98
  intent sync --context # Regenerate CLAUDE.md from your project page
99
99
  ```
100
100
 
101
+ `intent` is a short alias for typing. Another tool on your machine may also be
102
+ called `intent`, so everything the CLI writes for your agent (CLAUDE.md, slash
103
+ commands, hints) spells commands as `npx intentdocs …`, which always runs this
104
+ package.
105
+
101
106
  ---
102
107
 
103
108
  ## All Commands
@@ -106,8 +111,9 @@ intent sync --context # Regenerate CLAUDE.md from your project page
106
111
 
107
112
  | Command | Description |
108
113
  |---|---|
109
- | `intent login` | Authenticate (opens browser) |
114
+ | `intent login` | Authenticate (opens browser) — choose the workspace the token is bound to |
110
115
  | `intent logout` | Log out |
116
+ | `intent projects` | List the projects your token can reach — the whole workspace it is bound to |
111
117
  | `intent init` | Connect project directory to a story map + generate CLAUDE.md |
112
118
  | `intent setup` | Configure Claude Code / Cursor with slash commands |
113
119
  | `intent sync` | Refresh `INTENT.md` with latest stories |
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/index.js";
@@ -0,0 +1,170 @@
1
+ import {
2
+ getAuth
3
+ } from "./chunk-XZE2XP6O.js";
4
+
5
+ // src/lib/api.ts
6
+ var AuthError = class extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "AuthError";
10
+ }
11
+ };
12
+ function checkoutMismatch(config, binding) {
13
+ const checkout = config.workspace;
14
+ if (!checkout || !checkout.orgId) return null;
15
+ const checkoutName = checkout.name ?? "another workspace";
16
+ const here = config.mapTitle ? `"${config.mapTitle}"` : "this checkout";
17
+ if (!binding) {
18
+ return `${here} belongs to ${checkoutName}, but your token is personal. Run \`npx intentdocs login\` and choose ${checkoutName}.`;
19
+ }
20
+ if (binding.orgId !== checkout.orgId) {
21
+ const bound = binding.workspaceName ?? "a different workspace";
22
+ return `This checkout is ${checkoutName}'s \u2014 ${here} \u2014 but your token is bound to ${bound}. Run \`npx intentdocs login\` and choose ${checkoutName}, or work from a checkout of ${bound}'s.`;
23
+ }
24
+ return null;
25
+ }
26
+ function syncHeaders(init, auth = getAuth()) {
27
+ const headers = {};
28
+ if (init) {
29
+ new Headers(init).forEach((value, key) => {
30
+ headers[key] = value;
31
+ });
32
+ }
33
+ if (auth?.token) headers["Authorization"] = `Bearer ${auth.token}`;
34
+ return headers;
35
+ }
36
+ async function refusalMessage(res) {
37
+ try {
38
+ const body = await res.clone().json();
39
+ if (typeof body.message === "string") return body.message;
40
+ if (body.error && typeof body.error === "object") return body.error.hint ?? body.error.message ?? null;
41
+ } catch {
42
+ }
43
+ return null;
44
+ }
45
+ async function apiFetch(config, path, init) {
46
+ const auth = getAuth();
47
+ const mismatch = checkoutMismatch(config, auth?.binding);
48
+ if (mismatch) throw new AuthError(mismatch);
49
+ if (!auth?.token) {
50
+ throw new AuthError("Not logged in \u2014 every sync runs as you now. Run `npx intentdocs login`.");
51
+ }
52
+ const withBearer = { ...init, headers: syncHeaders(init?.headers, auth) };
53
+ const res = await fetch(apiUrl(config, path), withBearer);
54
+ if (res.status === 401) {
55
+ const said = await refusalMessage(res);
56
+ throw new AuthError(said ?? "Your login is no longer valid. Run `npx intentdocs login`.");
57
+ }
58
+ if (res.status === 404) {
59
+ const said = await refusalMessage(res);
60
+ if (said) throw new AuthError(said);
61
+ }
62
+ if (res.status === 403) {
63
+ const said = await refusalMessage(res);
64
+ throw new AuthError(
65
+ said ?? "Access denied \u2014 you may not have permission for this project. Run `npx intentdocs login` to re-authenticate."
66
+ );
67
+ }
68
+ if (!res.ok) throw new Error(`API error: ${res.status}`);
69
+ return res;
70
+ }
71
+ function apiUrl(config, path) {
72
+ if (!config.mapId) {
73
+ throw new AuthError("This checkout has no map id. Run `npx intentdocs init` to reconnect it.");
74
+ }
75
+ return `${config.apiBase}/api/sync/${config.mapId}${path}`;
76
+ }
77
+ async function listStories(config) {
78
+ const res = await apiFetch(config, "/stories");
79
+ return res.json();
80
+ }
81
+ async function getContext(config) {
82
+ const res = await apiFetch(config, "/context");
83
+ return res.text();
84
+ }
85
+ async function updateStories(config, stories) {
86
+ const res = await apiFetch(config, "/stories", {
87
+ method: "PATCH",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ stories })
90
+ });
91
+ return res.json();
92
+ }
93
+ async function uploadScreenshot(config, storyId, screenshot) {
94
+ try {
95
+ const form = new FormData();
96
+ form.append("story_id", storyId);
97
+ form.append(
98
+ "image",
99
+ new Blob([Uint8Array.from(screenshot.buffer)], { type: screenshot.mimeType }),
100
+ screenshot.filename
101
+ );
102
+ const res = await apiFetch(config, "/screenshots", {
103
+ method: "POST",
104
+ body: form
105
+ });
106
+ return await res.json();
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ async function createStories(config, stories) {
112
+ const res = await apiFetch(config, "/stories", {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ stories })
116
+ });
117
+ return res.json();
118
+ }
119
+ async function reportCriteria(config, storyId, results) {
120
+ const res = await apiFetch(config, "/criteria", {
121
+ method: "PATCH",
122
+ headers: { "Content-Type": "application/json" },
123
+ body: JSON.stringify({ story_id: storyId, results })
124
+ });
125
+ return res.json();
126
+ }
127
+ async function raiseBlocker(config, payload) {
128
+ const res = await apiFetch(config, "/blocker", {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ body: JSON.stringify(payload)
132
+ });
133
+ return res.json();
134
+ }
135
+ async function listDecisions(config) {
136
+ const res = await apiFetch(config, "/decisions");
137
+ return res.json();
138
+ }
139
+ async function amendDecision(config, decisionId, fields) {
140
+ const res = await apiFetch(config, `/decisions/${decisionId}`, {
141
+ method: "PATCH",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify(fields)
144
+ });
145
+ return res.json();
146
+ }
147
+ async function retractDecision(config, decisionId, reason) {
148
+ const res = await apiFetch(config, `/decisions/${decisionId}`, {
149
+ method: "DELETE",
150
+ headers: { "Content-Type": "application/json" },
151
+ body: JSON.stringify({ reason })
152
+ });
153
+ return res.json();
154
+ }
155
+
156
+ export {
157
+ syncHeaders,
158
+ apiFetch,
159
+ apiUrl,
160
+ listStories,
161
+ getContext,
162
+ updateStories,
163
+ uploadScreenshot,
164
+ createStories,
165
+ reportCriteria,
166
+ raiseBlocker,
167
+ listDecisions,
168
+ amendDecision,
169
+ retractDecision
170
+ };
@@ -0,0 +1,317 @@
1
+ import {
2
+ captureCli,
3
+ shutdownCliAnalytics
4
+ } from "./chunk-KPQ42YE6.js";
5
+ import {
6
+ requireConfig
7
+ } from "./chunk-JX2RLGOM.js";
8
+ import {
9
+ closePrompt,
10
+ confirm,
11
+ isInteractive
12
+ } from "./chunk-WRRUCCVZ.js";
13
+ import {
14
+ authenticatedFetch,
15
+ requireValidAuth
16
+ } from "./chunk-XZE2XP6O.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 `npx 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 `npx 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: npx 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
+ };
@@ -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 \`intent note ${story.id.slice(0, 8)} "..."\`_`,
127
+ `_Notes added via \`npx intentdocs note ${storyRef(story)} "..."\`_`,
90
128
  ``,
91
129
  `## Decisions`,
92
130
  ``,
93
- `_Log decisions via \`intent decide "..." -s ${story.id.slice(0, 8)}\`_`,
131
+ `_Log decisions via \`npx 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: \`intent done ${story.id.slice(0, 8)}\``
139
+ `Mark done: \`npx 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
- return JSON.parse(raw);
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
  }
@@ -34,7 +39,13 @@ function writeConfig(config) {
34
39
  function requireConfig() {
35
40
  const config = readConfig();
36
41
  if (!config) {
37
- console.error("Not initialized. Run: intent init");
42
+ console.error("Not initialized. Run: npx intentdocs init");
43
+ process.exit(1);
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 `npx intentdocs login` then `npx intentdocs init` to reconnect."
48
+ );
38
49
  process.exit(1);
39
50
  }
40
51
  return config;
@@ -0,0 +1,43 @@
1
+ import {
2
+ getAuth
3
+ } from "./chunk-XZE2XP6O.js";
4
+
5
+ // src/lib/analytics.ts
6
+ import { PostHog } from "posthog-node";
7
+ var client = null;
8
+ function getClient() {
9
+ if (client) return client;
10
+ const auth = getAuth();
11
+ if (!auth?.posthogKey) return null;
12
+ client = new PostHog(auth.posthogKey, {
13
+ host: auth.posthogHost ?? "https://eu.i.posthog.com",
14
+ flushAt: 1,
15
+ flushInterval: 0
16
+ });
17
+ return client;
18
+ }
19
+ function getDistinctId() {
20
+ return getAuth()?.userId ?? null;
21
+ }
22
+ function captureCli(event, properties, groups) {
23
+ const posthog = getClient();
24
+ const distinctId = getDistinctId();
25
+ if (!posthog || !distinctId) return;
26
+ posthog.capture({
27
+ distinctId,
28
+ event,
29
+ properties: {
30
+ ...properties,
31
+ ...groups?.project ? { project_id: groups.project } : {}
32
+ },
33
+ ...groups ? { groups } : {}
34
+ });
35
+ }
36
+ async function shutdownCliAnalytics() {
37
+ if (client) await client.shutdown();
38
+ }
39
+
40
+ export {
41
+ captureCli,
42
+ shutdownCliAnalytics
43
+ };