@popoverai/dotrequirements 0.25.0 → 0.26.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/dist/cli.js +8 -1
- package/dist/codebase-to-spec/present.d.ts +9 -0
- package/dist/codebase-to-spec/present.js +23 -2
- package/dist/codebase-to-spec/skill-install.d.ts +16 -6
- package/dist/codebase-to-spec/skill-install.js +109 -47
- package/dist/codebase-to-spec/version-check.d.ts +31 -0
- package/dist/codebase-to-spec/version-check.js +56 -0
- package/dist/commands/ai-setup.d.ts +12 -1
- package/dist/commands/ai-setup.js +65 -33
- package/dist/commands/codebase-to-spec/pack.d.ts +4 -0
- package/dist/commands/codebase-to-spec/pack.js +17 -0
- package/dist/commands/init.js +6 -1
- package/dist/commands/link-resolution.d.ts +79 -0
- package/dist/commands/link-resolution.js +141 -0
- package/dist/commands/link.d.ts +14 -4
- package/dist/commands/link.js +369 -16
- package/dist/commands/pull.js +19 -2
- package/dist/commands/push.js +36 -2
- package/dist/convex.d.ts +5 -3
- package/dist/convex.js +5 -3
- package/dist/harness/cache.d.ts +0 -14
- package/dist/harness/cache.js +1 -41
- package/dist/harness/finalize.js +2 -2
- package/dist/harness/prepare.js +1 -3
- package/dist/harness/requirementsLoader.d.ts +3 -3
- package/dist/harness/requirementsLoader.js +13 -8
- package/dist/mcp/handlers/authoring.d.ts +5 -5
- package/dist/mcp/handlers/authoring.js +9 -9
- package/dist/mcp/handlers/push.d.ts +2 -2
- package/dist/mcp/handlers/push.js +36 -3
- package/dist/mcp/handlers/review.d.ts +4 -4
- package/dist/mcp/handlers/review.js +4 -4
- package/dist/mcp/handlers/search.d.ts +1 -1
- package/dist/mcp/handlers/search.js +1 -1
- package/dist/mcp/index.js +29 -0
- package/dist/push/core.d.ts +18 -0
- package/dist/push/core.js +70 -3
- package/dist/push/index.d.ts +1 -1
- package/dist/push/index.js +1 -1
- package/dist/schema/parser-core.js +5 -1
- package/dist/schema/parser.js +5 -1
- package/dist/schema/run-marker.d.ts +38 -0
- package/dist/schema/run-marker.js +138 -0
- package/dist/schema/schemas.d.ts +12 -0
- package/dist/schema/schemas.js +1 -0
- package/dist/templates/agents/cts-worker.md +1 -1
- package/dist/templates/skills/codebase-to-spec/SKILL.md +33 -11
- package/dist/templates/workflows/specify-codebase.js +4 -2
- package/dist/utils/own-package.d.ts +10 -0
- package/dist/utils/own-package.js +13 -0
- package/dist/utils/project-selector.d.ts +5 -0
- package/dist/utils/project-selector.js +4 -0
- package/package.json +2 -2
package/dist/commands/link.js
CHANGED
|
@@ -1,25 +1,314 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import { ConvexHttpClient } from "convex/browser";
|
|
2
4
|
import { getConvexUrl } from "../config.js";
|
|
5
|
+
import { api } from "../convex.js";
|
|
6
|
+
import { WEB_APP_URL } from "../push/index.js";
|
|
3
7
|
import { brand } from "../utils/brand.js";
|
|
4
8
|
import { ensureGitignore } from "../utils/gitignore.js";
|
|
5
9
|
import { executeOAuthFlow } from "../utils/oauth-flow.js";
|
|
6
|
-
import { getOrCreateProjectSecret, promptExpiryDays,
|
|
10
|
+
import { getOrCreateProjectSecret, promptExpiryDays, selectTeam, } from "../utils/project-selector.js";
|
|
7
11
|
import { findProjectRoot, readProjectSettings, writeProjectSettings, } from "../utils/project-settings.js";
|
|
8
12
|
import { promptChoice, promptConfirm } from "../utils/prompts.js";
|
|
13
|
+
import { hasCapacity, isNonInteractive, resolveProject, resolveProjectName, resolveTeam, } from "./link-resolution.js";
|
|
9
14
|
import { pullCommand } from "./pull.js";
|
|
10
15
|
/**
|
|
11
|
-
* Link command - connect or reconnect local project to cloud
|
|
16
|
+
* Link command - connect or reconnect local project to cloud, creating the
|
|
17
|
+
* cloud project when needed. Link owns the "upgrade local to cloud"
|
|
18
|
+
* transition.
|
|
12
19
|
*
|
|
13
20
|
* Requirements:
|
|
14
21
|
* - LINK-1: Requires existing .requirements/ folder
|
|
15
22
|
* - LINK-2: Offers refresh when credentials exist
|
|
16
23
|
* - LINK-3: Uses inline OAuth (no persistent tokens)
|
|
17
|
-
* - LINK-4, LINK-5: Team and project selection
|
|
24
|
+
* - LINK-4, LINK-5: Team and project selection (including the create option)
|
|
18
25
|
* - LINK-6: Saves credentials to project-settings.json
|
|
19
|
-
* - LINK-7: Optionally pulls requirements
|
|
26
|
+
* - LINK-7: Optionally pulls requirements (interactive only)
|
|
20
27
|
* - LINK-8: Handles network errors gracefully
|
|
28
|
+
* - LINK-9: Creates the cloud project when chosen
|
|
29
|
+
* - LINK-10/11/12: Non-interactive mode for AI-driven setup
|
|
30
|
+
* - LINK-13: Non-interactive success includes share artifacts (team-invite
|
|
31
|
+
* URL + read-only pull command) so one "yes" puts the spec in front of
|
|
32
|
+
* the team
|
|
21
33
|
*/
|
|
22
|
-
export async function linkCommand() {
|
|
34
|
+
export async function linkCommand(options = {}) {
|
|
35
|
+
if (isNonInteractive(options)) {
|
|
36
|
+
await runNonInteractiveLink(options);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await runInteractiveLink(options);
|
|
40
|
+
}
|
|
41
|
+
const EXIT_DECISION_NEEDED = 2;
|
|
42
|
+
async function runNonInteractiveLink(options) {
|
|
43
|
+
// LINK-10.5/11.3: in JSON mode the single stdout line is the result object;
|
|
44
|
+
// every informational message (including the OAuth flow's, and the browser
|
|
45
|
+
// fallback URL) goes to stderr so the output stays machine-parseable.
|
|
46
|
+
const originalLog = console.log;
|
|
47
|
+
if (options.json) {
|
|
48
|
+
console.log = (...args) => console.error(...args);
|
|
49
|
+
}
|
|
50
|
+
const finish = (outcome) => {
|
|
51
|
+
console.log = originalLog;
|
|
52
|
+
if (options.json) {
|
|
53
|
+
originalLog(JSON.stringify(outcome));
|
|
54
|
+
}
|
|
55
|
+
else if (outcome.status === "decision_needed") {
|
|
56
|
+
console.log(`\n${outcome.message}`);
|
|
57
|
+
for (const opt of outcome.options) {
|
|
58
|
+
console.log(` - ${opt.label}${opt.retryFlag ? ` (retry with: dotrequirements link ${opt.retryFlag})` : ""}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (outcome.status === "error") {
|
|
62
|
+
console.error(`\n✗ ${outcome.message}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(`\n✓ Linked to project: ${outcome.projectName}${outcome.renamed ? " (renamed to match this repository)" : ""}`);
|
|
66
|
+
console.log(` Project ID: ${outcome.projectSlug}`);
|
|
67
|
+
// LINK-13: hand over the share mechanics, one per recipient surface
|
|
68
|
+
if (outcome.inviteUrl) {
|
|
69
|
+
console.log(` Invite a teammate (web): ${outcome.inviteUrl}`);
|
|
70
|
+
}
|
|
71
|
+
if (outcome.sharePullCommand) {
|
|
72
|
+
console.log(` Share read-only to an IDE: ${outcome.sharePullCommand}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (outcome.status === "decision_needed") {
|
|
76
|
+
process.exitCode = EXIT_DECISION_NEEDED;
|
|
77
|
+
}
|
|
78
|
+
else if (outcome.status === "error") {
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
// LINK-1: same precondition as interactive
|
|
84
|
+
const cwd = process.cwd();
|
|
85
|
+
const projectRoot = findProjectRoot(cwd);
|
|
86
|
+
if (!projectRoot || projectRoot !== cwd) {
|
|
87
|
+
finish({
|
|
88
|
+
status: "error",
|
|
89
|
+
message: 'No .requirements/ folder found. Run "dotrequirements init" to initialize a project first.',
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// LINK-10.6: existing credentials default to refresh-same-project
|
|
94
|
+
let refreshTargetSlug;
|
|
95
|
+
try {
|
|
96
|
+
const existing = readProjectSettings(projectRoot);
|
|
97
|
+
refreshTargetSlug = existing?.projectId;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
refreshTargetSlug = undefined;
|
|
101
|
+
}
|
|
102
|
+
// LINK-3 / LINK-10.7: browser auth is the only human step
|
|
103
|
+
const oauthResult = await executeOAuthFlow();
|
|
104
|
+
const client = new ConvexHttpClient(getConvexUrl());
|
|
105
|
+
client.setAuth(oauthResult.accessToken);
|
|
106
|
+
await client.mutation(api.teams.mutations.ensureDefaultTeam, {});
|
|
107
|
+
const teams = (await client.query(api.teams.queries.listForUserWithUsage));
|
|
108
|
+
if (teams.length === 0) {
|
|
109
|
+
finish({
|
|
110
|
+
status: "error",
|
|
111
|
+
message: "You are not a member of any teams. Create one at https://app.dotrequirements.io/teams/new and retry.",
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// LINK-10.6: refresh path — find the already-linked project. A deviation
|
|
116
|
+
// flag (--team/--connect/--create, LINK-12) overrides the refresh default
|
|
117
|
+
// and falls through to normal resolution. Settings store the raw
|
|
118
|
+
// projectId when a project has no slug, so match either.
|
|
119
|
+
if (refreshTargetSlug &&
|
|
120
|
+
!options.connect &&
|
|
121
|
+
!options.create &&
|
|
122
|
+
!options.team) {
|
|
123
|
+
for (const team of teams) {
|
|
124
|
+
const projects = await listProjects(client, team.teamId);
|
|
125
|
+
const match = projects.find((p) => p.projectSlug === refreshTargetSlug ||
|
|
126
|
+
p.projectId === refreshTargetSlug);
|
|
127
|
+
if (match) {
|
|
128
|
+
const settingsPath = await connectToProject(client, projectRoot, match.projectId, refreshTargetSlug);
|
|
129
|
+
// LINK-13.2: ensureShareToken is idempotent, so a refresh hands
|
|
130
|
+
// back the same share command rather than minting a new token
|
|
131
|
+
const artifacts = await mintShareArtifacts(client, team.teamId, match.projectId);
|
|
132
|
+
finish({
|
|
133
|
+
status: "linked",
|
|
134
|
+
projectSlug: refreshTargetSlug,
|
|
135
|
+
projectName: match.projectName,
|
|
136
|
+
renamed: false,
|
|
137
|
+
settingsPath,
|
|
138
|
+
...artifacts,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
finish({
|
|
144
|
+
status: "error",
|
|
145
|
+
message: `Currently-linked project "${refreshTargetSlug}" was not found in your teams. Pass --connect <slug> to link a different project.`,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const teamResolution = resolveTeam(teams, options);
|
|
150
|
+
if (teamResolution.kind === "no-match") {
|
|
151
|
+
finish({
|
|
152
|
+
status: "error",
|
|
153
|
+
message: `No team matched "${teamResolution.provided}". Your teams: ${teamResolution.alternatives.join(", ")}`,
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (teamResolution.kind === "decision") {
|
|
158
|
+
const { kind: _kind, ...decision } = teamResolution;
|
|
159
|
+
finish({ status: "decision_needed", ...decision });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const team = teamResolution.team;
|
|
163
|
+
const projects = await listProjects(client, team.teamId);
|
|
164
|
+
const projectResolution = resolveProject(projects, team, options);
|
|
165
|
+
if (projectResolution.kind === "no-match") {
|
|
166
|
+
finish({
|
|
167
|
+
status: "error",
|
|
168
|
+
message: `No project matched "${projectResolution.provided}". Projects in ${team.name}: ${projectResolution.alternatives.join(", ") || "(none)"}`,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (projectResolution.kind === "decision") {
|
|
173
|
+
const { kind: _kind, ...decision } = projectResolution;
|
|
174
|
+
finish({ status: "decision_needed", ...decision });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (projectResolution.kind === "create") {
|
|
178
|
+
// LINK-9 via LINK-12.2 (or zero-projects default)
|
|
179
|
+
const name = resolveProjectName(options, readPackageName(cwd), path.basename(cwd));
|
|
180
|
+
const created = (await client.mutation(api.projects.mutations.create, {
|
|
181
|
+
name,
|
|
182
|
+
teamId: team.teamId,
|
|
183
|
+
}));
|
|
184
|
+
const settingsPath = await connectToProject(client, projectRoot, created.projectId, created.slug);
|
|
185
|
+
const artifacts = await mintShareArtifacts(client, team.teamId, created.projectId);
|
|
186
|
+
finish({
|
|
187
|
+
status: "linked",
|
|
188
|
+
projectSlug: created.slug,
|
|
189
|
+
projectName: name,
|
|
190
|
+
renamed: false,
|
|
191
|
+
settingsPath,
|
|
192
|
+
...artifacts,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// connect (possibly renaming the empty default project, LINK-10.2)
|
|
197
|
+
let projectName = projectResolution.project.projectName;
|
|
198
|
+
let renamed = false;
|
|
199
|
+
if (projectResolution.rename) {
|
|
200
|
+
const newName = resolveProjectName(options, readPackageName(cwd), path.basename(cwd));
|
|
201
|
+
try {
|
|
202
|
+
await client.mutation(api.projects.mutations.update, {
|
|
203
|
+
target: {
|
|
204
|
+
type: "project",
|
|
205
|
+
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
|
|
206
|
+
id: projectResolution.project.projectId,
|
|
207
|
+
},
|
|
208
|
+
name: newName,
|
|
209
|
+
});
|
|
210
|
+
projectName = newName;
|
|
211
|
+
renamed = true;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
// LINK-10.9: renaming is admin-gated server-side — a non-admin member
|
|
215
|
+
// (e.g. invited via init --invite) still gets connected, just without
|
|
216
|
+
// the rename
|
|
217
|
+
console.error(`Note: couldn't rename the default project to "${newName}" (team admin required) — connected without renaming.`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const slug = projectResolution.project.projectSlug ??
|
|
221
|
+
projectResolution.project.projectId;
|
|
222
|
+
const settingsPath = await connectToProject(client, projectRoot, projectResolution.project.projectId, slug);
|
|
223
|
+
const artifacts = await mintShareArtifacts(client, team.teamId, projectResolution.project.projectId);
|
|
224
|
+
finish({
|
|
225
|
+
status: "linked",
|
|
226
|
+
projectSlug: projectResolution.project.projectSlug,
|
|
227
|
+
projectName,
|
|
228
|
+
renamed,
|
|
229
|
+
settingsPath,
|
|
230
|
+
...artifacts,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
finish({
|
|
235
|
+
status: "error",
|
|
236
|
+
message: error instanceof Error ? error.message : String(error),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
console.log = originalLog;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* LINK-13: mint the artifacts that put the spec in front of the team — a
|
|
245
|
+
* single-use team-invite URL (web reviewers) and a read-only pull command
|
|
246
|
+
* (IDE teammates). Either mint may fail without failing the connection
|
|
247
|
+
* (LINK-13.3): createInvite is admin-gated, so non-admin members link
|
|
248
|
+
* successfully and the invite URL is simply omitted.
|
|
249
|
+
*/
|
|
250
|
+
async function mintShareArtifacts(client, teamId, projectId) {
|
|
251
|
+
const artifacts = {};
|
|
252
|
+
try {
|
|
253
|
+
const invite = (await client.mutation(api.teamInvites.mutations.createInvite,
|
|
254
|
+
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
|
|
255
|
+
{ target: { type: "team", id: teamId } }));
|
|
256
|
+
artifacts.inviteUrl = `${WEB_APP_URL}/invite/${invite.token}`;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// LINK-13.3: admin-gated — omit the invite URL for non-admin members
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const share = (await client.mutation(api.projectSecrets.mutations.ensureShareToken,
|
|
263
|
+
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
|
|
264
|
+
{ target: { type: "project", id: projectId } }));
|
|
265
|
+
artifacts.sharePullCommand = `npx -y @popoverai/dotrequirements pull --share ${share.token}`;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// LINK-13.3: connection already succeeded — omit rather than fail
|
|
269
|
+
}
|
|
270
|
+
return artifacts;
|
|
271
|
+
}
|
|
272
|
+
async function listProjects(client, teamId) {
|
|
273
|
+
const projects = (await client.query(api.projectSecrets.queries.listOwnSecretsForTeam,
|
|
274
|
+
// biome-ignore lint/suspicious/noExplicitAny: CLI workspace doesn't import convex's branded Id type; the runtime value is a plain string the server validates.
|
|
275
|
+
{ target: { type: "team", id: teamId } }));
|
|
276
|
+
return projects.map((p) => ({
|
|
277
|
+
projectId: p.projectId,
|
|
278
|
+
projectName: p.projectName,
|
|
279
|
+
projectSlug: p.projectSlug,
|
|
280
|
+
isDefault: p.isDefault ?? false,
|
|
281
|
+
// Older servers don't report content state; treat unknown as content so
|
|
282
|
+
// we never rename something we can't vouch for
|
|
283
|
+
hasUserContent: p.hasUserContent ?? true,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Mint (or fetch) a secret and write credentials (LINK-6). Returns the
|
|
288
|
+
* settings file path. Non-interactive: tier-default expiry, no prompts.
|
|
289
|
+
*/
|
|
290
|
+
async function connectToProject(client, projectRoot, projectId, slug) {
|
|
291
|
+
const secretResult = await getOrCreateProjectSecret(client, projectId, undefined);
|
|
292
|
+
writeProjectSettings(projectRoot, {
|
|
293
|
+
projectId: slug,
|
|
294
|
+
projectSecret: secretResult.secret,
|
|
295
|
+
});
|
|
296
|
+
ensureGitignore(projectRoot);
|
|
297
|
+
return path.join(projectRoot, ".requirements", "project-settings.json");
|
|
298
|
+
}
|
|
299
|
+
function readPackageName(cwd) {
|
|
300
|
+
try {
|
|
301
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
302
|
+
return typeof pkg.name === "string" ? pkg.name : undefined;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ============================================================================
|
|
309
|
+
// Interactive mode
|
|
310
|
+
// ============================================================================
|
|
311
|
+
async function runInteractiveLink(options) {
|
|
23
312
|
try {
|
|
24
313
|
console.log(`Linking to ${brand} cloud...\n`);
|
|
25
314
|
const cwd = process.cwd();
|
|
@@ -65,19 +354,38 @@ export async function linkCommand() {
|
|
|
65
354
|
console.log("Link cancelled.");
|
|
66
355
|
return;
|
|
67
356
|
}
|
|
68
|
-
// LINK-5: Select project
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
357
|
+
// LINK-5: Select project — or create one (LINK-5.1, LINK-9)
|
|
358
|
+
const selection = await selectProjectOrCreate(client, selectedTeam);
|
|
359
|
+
if (selection === null) {
|
|
360
|
+
console.log("Link cancelled.");
|
|
72
361
|
return;
|
|
73
362
|
}
|
|
363
|
+
let projectId;
|
|
364
|
+
let projectName;
|
|
365
|
+
let projectSlugOrId;
|
|
366
|
+
if (selection === "create") {
|
|
367
|
+
// LINK-9: create with the detected default name (-n/--name overrides)
|
|
368
|
+
const defaultName = resolveProjectName(options, readPackageName(cwd), path.basename(cwd));
|
|
369
|
+
console.log(`\nCreating project "${defaultName}"...`);
|
|
370
|
+
const created = (await client.mutation(api.projects.mutations.create, {
|
|
371
|
+
name: defaultName,
|
|
372
|
+
teamId: selectedTeam.teamId,
|
|
373
|
+
}));
|
|
374
|
+
projectId = created.projectId;
|
|
375
|
+
projectName = defaultName;
|
|
376
|
+
projectSlugOrId = created.slug;
|
|
377
|
+
console.log(`✓ Created project: ${created.slug}`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
projectId = selection.projectId;
|
|
381
|
+
projectName = selection.projectName;
|
|
382
|
+
projectSlugOrId = selection.projectSlug || selection.projectId;
|
|
383
|
+
}
|
|
74
384
|
// AUTHZ-3: Prompt for secret expiry (paid tier only)
|
|
75
385
|
const expiryDays = await promptExpiryDays(selectedTeam.tier);
|
|
76
386
|
// LINK-5.3: Get or create secret for this project
|
|
77
387
|
console.log("\nGetting project credentials...");
|
|
78
|
-
const secretResult = await getOrCreateProjectSecret(client,
|
|
79
|
-
const projectId = selectedProject.projectSlug || selectedProject.projectId;
|
|
80
|
-
const secret = secretResult.secret;
|
|
388
|
+
const secretResult = await getOrCreateProjectSecret(client, projectId, expiryDays);
|
|
81
389
|
if (secretResult.created) {
|
|
82
390
|
console.log("✓ Created new project secret");
|
|
83
391
|
}
|
|
@@ -85,18 +393,21 @@ export async function linkCommand() {
|
|
|
85
393
|
console.log("✓ Retrieved existing project secret");
|
|
86
394
|
}
|
|
87
395
|
// LINK-6: Write credentials to project-settings.json
|
|
88
|
-
writeProjectSettings(projectRoot, {
|
|
396
|
+
writeProjectSettings(projectRoot, {
|
|
397
|
+
projectId: projectSlugOrId,
|
|
398
|
+
projectSecret: secretResult.secret,
|
|
399
|
+
});
|
|
89
400
|
ensureGitignore(projectRoot);
|
|
90
401
|
console.log("✓ Wrote credentials to .requirements/project-settings.json");
|
|
91
402
|
// LINK-7: Optionally pull requirements
|
|
92
403
|
const shouldPull = await promptConfirm("Pull requirements from cloud now?", true);
|
|
93
404
|
if (shouldPull) {
|
|
94
405
|
console.log("\nPulling requirements...");
|
|
95
|
-
await pullCommand({ project:
|
|
406
|
+
await pullCommand({ project: projectSlugOrId });
|
|
96
407
|
}
|
|
97
408
|
// LINK-7.2: Success message
|
|
98
|
-
console.log(`\n✓ Successfully linked to project: ${
|
|
99
|
-
console.log(` Project ID: ${
|
|
409
|
+
console.log(`\n✓ Successfully linked to project: ${projectName}`);
|
|
410
|
+
console.log(` Project ID: ${projectSlugOrId}`);
|
|
100
411
|
console.log("\nDocumentation: https://docs.dotrequirements.io/getting-started");
|
|
101
412
|
}
|
|
102
413
|
catch (error) {
|
|
@@ -109,6 +420,48 @@ export async function linkCommand() {
|
|
|
109
420
|
process.exit(1);
|
|
110
421
|
}
|
|
111
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* LINK-5: project selection including the create option (LINK-5.1) when the
|
|
425
|
+
* team has capacity. Returns the chosen project, "create", or null (cancel).
|
|
426
|
+
*/
|
|
427
|
+
async function selectProjectOrCreate(client, team) {
|
|
428
|
+
console.log("\nFetching projects...\n");
|
|
429
|
+
const projects = await listProjects(client, team.teamId);
|
|
430
|
+
const teamOption = {
|
|
431
|
+
teamId: team.teamId,
|
|
432
|
+
name: team.name ?? "this team",
|
|
433
|
+
tier: team.tier,
|
|
434
|
+
tierName: team.tierName ?? team.tier,
|
|
435
|
+
projectCount: team.projectCount ?? projects.length,
|
|
436
|
+
projectLimit: team.projectLimit ?? -1,
|
|
437
|
+
};
|
|
438
|
+
const canCreate = hasCapacity(teamOption);
|
|
439
|
+
if (projects.length === 0) {
|
|
440
|
+
if (!canCreate) {
|
|
441
|
+
// LINK-5.2: connect is impossible; the remedies live elsewhere
|
|
442
|
+
console.log("This team has no projects and is at its project limit.");
|
|
443
|
+
console.log("Delete a project you no longer need (in the web app) or upgrade the team, then retry.\n");
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const confirmed = await promptConfirm("This team has no projects. Create a new one?", true);
|
|
447
|
+
return confirmed ? "create" : null;
|
|
448
|
+
}
|
|
449
|
+
const choices = projects.map((p) => ({
|
|
450
|
+
title: p.projectSlug
|
|
451
|
+
? `${p.projectName} (${p.projectSlug})`
|
|
452
|
+
: p.projectName,
|
|
453
|
+
value: p.projectId,
|
|
454
|
+
}));
|
|
455
|
+
if (canCreate) {
|
|
456
|
+
choices.push({ title: "Create a new project", value: "CREATE_NEW" });
|
|
457
|
+
}
|
|
458
|
+
const selected = await promptChoice("Select a project:", choices);
|
|
459
|
+
if (!selected)
|
|
460
|
+
return null;
|
|
461
|
+
if (selected === "CREATE_NEW")
|
|
462
|
+
return "create";
|
|
463
|
+
return projects.find((p) => p.projectId === selected) ?? null;
|
|
464
|
+
}
|
|
112
465
|
/**
|
|
113
466
|
* LINK-2: Handle existing credentials - offer refresh or connect to different project
|
|
114
467
|
* Returns true if user wants to continue (either refresh or different project)
|
package/dist/commands/pull.js
CHANGED
|
@@ -32,9 +32,14 @@ export async function pullCommand(options) {
|
|
|
32
32
|
console.log(`\nProject: "${projectName}"`);
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
|
-
// SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully
|
|
35
|
+
// SHARE-TOKEN-CLI-2: Handle invalid/revoked tokens gracefully.
|
|
36
|
+
// Production redacts ConvexError messages to "Server Error" but
|
|
37
|
+
// preserves error.data — check both, or the friendly message only
|
|
38
|
+
// ever appears against dev deployments.
|
|
39
|
+
const data = error.data;
|
|
36
40
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
37
|
-
if (
|
|
41
|
+
if (data?.message === "Invalid share token" ||
|
|
42
|
+
errorMessage.includes("Invalid share token")) {
|
|
38
43
|
throw new Error("Invalid share token. The token may be incorrect or has been revoked.\n" +
|
|
39
44
|
"Please request a new share token from your team.");
|
|
40
45
|
}
|
|
@@ -96,11 +101,23 @@ export async function pullCommand(options) {
|
|
|
96
101
|
const filePath = existingFilePath ??
|
|
97
102
|
path.join(requirementsDir, `${sanitizeFileName(doc.title)}.requirements.md`);
|
|
98
103
|
const fileName = path.basename(filePath);
|
|
104
|
+
// IMPORT-1: carry the CTS run marker forward from the existing local
|
|
105
|
+
// file — pull rebuilds frontmatter from cloud data, and silently dropping
|
|
106
|
+
// the marker would strip a committed spec's import credential before a
|
|
107
|
+
// teammate ever pushes it
|
|
108
|
+
let existingCtsRun;
|
|
109
|
+
if (existingFilePath && fs.existsSync(existingFilePath)) {
|
|
110
|
+
const match = fs
|
|
111
|
+
.readFileSync(existingFilePath, "utf-8")
|
|
112
|
+
.match(/^ctsRun: (.+)$/m);
|
|
113
|
+
existingCtsRun = match?.[1].trim();
|
|
114
|
+
}
|
|
99
115
|
// Build metadata with pulledAt for conflict detection
|
|
100
116
|
// SYNC-CLI-EDIT-2: pulledAt is compared with cloud updatedAt during push
|
|
101
117
|
const metadata = {
|
|
102
118
|
pulledAt: new Date().toISOString(),
|
|
103
119
|
version: doc.version,
|
|
120
|
+
...(existingCtsRun ? { ctsRun: existingCtsRun } : {}),
|
|
104
121
|
document: {
|
|
105
122
|
id: doc.documentId,
|
|
106
123
|
title: doc.title,
|
package/dist/commands/push.js
CHANGED
|
@@ -100,16 +100,50 @@ export async function pushCommand(file, options) {
|
|
|
100
100
|
console.log(` ✓ Updated: ${fileName}`);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
// IMPORT-3: marker failures are loud but never fatal
|
|
104
|
+
if (result.importWarnings.length > 0) {
|
|
105
|
+
console.log();
|
|
106
|
+
for (const { fileName, status } of result.importWarnings) {
|
|
107
|
+
if (status === "already_used") {
|
|
108
|
+
console.log(`⚠ ${fileName}: this file's import marker was already used, so its requirements were saved as natively authored (they count toward your plan's requirement limit). Re-running codebase-to-spec produces a fresh marker.`);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log(`⚠ ${fileName}: this file's import marker was not recognized — your CLI may need updating. Its requirements were saved as natively authored (they count toward your plan's requirement limit).`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// SYNC-FAIL-1: a failed push must not masquerade as success — agents and
|
|
116
|
+
// scripts drive this command and rely on the exit code to tell the truth
|
|
117
|
+
const failed = result.errors.length;
|
|
118
|
+
const syncedCount = result.created + result.updated;
|
|
119
|
+
if (failed > 0) {
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
if (syncedCount === 0) {
|
|
122
|
+
console.log(`\n✗ Push failed: ${failed} document(s) could not be saved.`);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
console.log(`\n⚠ Push incomplete: ${syncedCount} document(s) synced, ${failed} failed.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(`\n✓ Push complete!`);
|
|
130
|
+
}
|
|
104
131
|
if (result.created > 0) {
|
|
105
132
|
console.log(` Created: ${result.created} document(s)`);
|
|
106
133
|
}
|
|
107
134
|
if (result.updated > 0) {
|
|
108
135
|
console.log(` Updated: ${result.updated} document(s)`);
|
|
109
136
|
}
|
|
110
|
-
if (
|
|
137
|
+
if (failed === 0 && syncedCount === 0) {
|
|
111
138
|
console.log(" No documents were pushed.");
|
|
112
139
|
}
|
|
140
|
+
// SYNC-LAND-1: land the user on their spec, not at a dead end
|
|
141
|
+
if (result.synced.length > 0) {
|
|
142
|
+
console.log("\nReview in dot•requirements:");
|
|
143
|
+
for (const doc of result.synced) {
|
|
144
|
+
console.log(` ${doc.fileName} → ${doc.url}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
113
147
|
}
|
|
114
148
|
/**
|
|
115
149
|
* Display the dry run summary.
|
package/dist/convex.d.ts
CHANGED
|
@@ -34,13 +34,11 @@ export declare const api: {
|
|
|
34
34
|
queries: {
|
|
35
35
|
listByProject: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
36
36
|
};
|
|
37
|
-
mutations: {
|
|
38
|
-
bulkUpdate: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
39
|
-
};
|
|
40
37
|
};
|
|
41
38
|
projects: {
|
|
42
39
|
mutations: {
|
|
43
40
|
create: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
41
|
+
update: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
44
42
|
};
|
|
45
43
|
};
|
|
46
44
|
teams: {
|
|
@@ -55,6 +53,9 @@ export declare const api: {
|
|
|
55
53
|
queries: {
|
|
56
54
|
getInviteInfo: import("convex/server").FunctionReference<"query", "public", any, any, string | undefined>;
|
|
57
55
|
};
|
|
56
|
+
mutations: {
|
|
57
|
+
createInvite: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
58
|
+
};
|
|
58
59
|
};
|
|
59
60
|
polar: {
|
|
60
61
|
acceptTeamInvite: import("convex/server").FunctionReference<"action", "public", any, any, string | undefined>;
|
|
@@ -67,6 +68,7 @@ export declare const api: {
|
|
|
67
68
|
mutations: {
|
|
68
69
|
ensureOwnSecret: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
69
70
|
revokeOwnSecret: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
71
|
+
ensureShareToken: import("convex/server").FunctionReference<"mutation", "public", any, any, string | undefined>;
|
|
70
72
|
};
|
|
71
73
|
};
|
|
72
74
|
lib: {
|
package/dist/convex.js
CHANGED
|
@@ -40,13 +40,11 @@ export const api = {
|
|
|
40
40
|
queries: {
|
|
41
41
|
listByProject: query("requirements/queries:listByProject"),
|
|
42
42
|
},
|
|
43
|
-
mutations: {
|
|
44
|
-
bulkUpdate: mutation("requirements/mutations:bulkUpdate"),
|
|
45
|
-
},
|
|
46
43
|
},
|
|
47
44
|
projects: {
|
|
48
45
|
mutations: {
|
|
49
46
|
create: mutation("projects/mutations:create"),
|
|
47
|
+
update: mutation("projects/mutations:update"),
|
|
50
48
|
},
|
|
51
49
|
},
|
|
52
50
|
teams: {
|
|
@@ -61,6 +59,9 @@ export const api = {
|
|
|
61
59
|
queries: {
|
|
62
60
|
getInviteInfo: query("teamInvites/queries:getInviteInfo"),
|
|
63
61
|
},
|
|
62
|
+
mutations: {
|
|
63
|
+
createInvite: mutation("teamInvites/mutations:createInvite"),
|
|
64
|
+
},
|
|
64
65
|
},
|
|
65
66
|
polar: {
|
|
66
67
|
acceptTeamInvite: action("polar:acceptTeamInvite"),
|
|
@@ -73,6 +74,7 @@ export const api = {
|
|
|
73
74
|
mutations: {
|
|
74
75
|
ensureOwnSecret: mutation("projectSecrets/mutations:ensureOwnSecret"),
|
|
75
76
|
revokeOwnSecret: mutation("projectSecrets/mutations:revokeOwnSecret"),
|
|
77
|
+
ensureShareToken: mutation("projectSecrets/mutations:ensureShareToken"),
|
|
76
78
|
},
|
|
77
79
|
},
|
|
78
80
|
lib: {
|
package/dist/harness/cache.d.ts
CHANGED
|
@@ -94,20 +94,6 @@ export declare function getTestRunId(requirementsDir: string): string | null;
|
|
|
94
94
|
* Clean up the test run ID file
|
|
95
95
|
*/
|
|
96
96
|
export declare function cleanupTestRunId(requirementsDir: string): void;
|
|
97
|
-
/**
|
|
98
|
-
* Write the project root to cache (for cross-process persistence).
|
|
99
|
-
* This allows test workers to find the lookup cache even after cwd changes.
|
|
100
|
-
*/
|
|
101
|
-
export declare function writeProjectRoot(requirementsDir: string, projectRoot: string): void;
|
|
102
|
-
/**
|
|
103
|
-
* Read the project root from cache, returning null if not found.
|
|
104
|
-
*/
|
|
105
|
-
export declare function readProjectRoot(requirementsDir: string): string | null;
|
|
106
|
-
/**
|
|
107
|
-
* Try to find the project root from any known .requirements cache directory.
|
|
108
|
-
* Walks up from cwd looking for .requirements/.cache/project-root file.
|
|
109
|
-
*/
|
|
110
|
-
export declare function findCachedProjectRoot(startDir?: string): string | null;
|
|
111
97
|
/**
|
|
112
98
|
* Clear tracking data for a new test run
|
|
113
99
|
*/
|