@407dev/cli 0.1.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 ADDED
@@ -0,0 +1,79 @@
1
+ # @407dev/cli
2
+
3
+ ## Purpose
4
+
5
+ - Command-line tool for CLI login (`cms login`), project linking (`cms link`), static AST manifest extraction (`cms extract`), template & schema validation (`cms check`), code-first manifest synchronization (`cms push`), and entry migrations.
6
+
7
+ ## Surface
8
+
9
+ - Binary runner: [src/index.ts](src/index.ts) (`cms`, `runCli`, `printHelp`)
10
+ - Auth resolution: [src/auth.ts](src/auth.ts) (`resolveAuthToken`)
11
+ - Credentials store: [src/credentials.ts](src/credentials.ts) (`saveStoredCredentials`, `getStoredCredentials`, `removeStoredCredentials`, `refreshCredentialsIfNeeded`)
12
+ - Login command: [src/commands/login.ts](src/commands/login.ts) (`executeLogin`)
13
+ - Logout command: [src/commands/logout.ts](src/commands/logout.ts) (`executeLogout`)
14
+ - Whoami command: [src/commands/whoami.ts](src/commands/whoami.ts) (`executeWhoami`)
15
+ - Link command: [src/commands/link.ts](src/commands/link.ts) (`executeLink`, `mergeEnvContent`, `mergeEnvFile`)
16
+ - Push command: [src/commands/push.ts](src/commands/push.ts) (`executePush`)
17
+ - Check command: [src/commands/check.ts](src/commands/check.ts) (`executeCheck`)
18
+ - Extract command: [src/commands/extract.ts](src/commands/extract.ts) (`executeExtract`)
19
+ - Skills command: [src/commands/skills.ts](src/commands/skills.ts) (`executeSkillsInstall`, `installSkills`, `listBundledSkills`)
20
+ - Bundled agent skills: [skills/](skills) (`407dev-cms-setup`, `407dev-cms-keying`, `407dev-cms-fields`)
21
+ - AST parser & sourcemap tracer: [src/extract/parse.ts](src/extract/parse.ts) (`parseFile`, `globSourceFiles`)
22
+ - Scope & group resolution: [src/extract/resolve.ts](src/extract/resolve.ts) (`resolveFileContext`)
23
+ - Field & collection collector: [src/extract/collect.ts](src/extract/collect.ts) (`collectFromFile`)
24
+ - Merge & conflict detection: [src/extract/merge.ts](src/extract/merge.ts) (`mergeExtractionResults`)
25
+ - Manifest builder: [src/extract/manifest.ts](src/extract/manifest.ts) (`buildSiteManifest`)
26
+ - Derived routes resolver: [src/extract/routes.ts](src/extract/routes.ts) (`loadDerivedRoutes`)
27
+ - Entry migrations runner: [src/migrations.ts](src/migrations.ts) (`runEntryMigrations`)
28
+ - State store: [src/state-store.ts](src/state-store.ts) (`StateStore`)
29
+ - Extractor types: [src/extract/types.ts](src/extract/types.ts)
30
+
31
+ ## Commands
32
+
33
+ - `cms login`: Starts local HTTP server, opens browser to `/cli/authorize`, and receives isolated session credentials into `~/.config/407dev/credentials.json` (supports `--api-url`).
34
+ - `cms logout`: Removes stored credentials for target API URL (`--api-url`).
35
+ - `cms whoami`: Displays current authentication identity and target API URL (`--api-url`).
36
+ - `cms link`: Links local codebase to a remote site (`--site <id>`, `--api-url <url>`). Interactively prompts if `--site` is omitted, updates `.env` (`CMS_SITE_ID`, `CMS_API_URL`, `CMS_CONTENT_URL`), and checks `astro.config.mjs`.
37
+ - `cms check`: Statically scans `.astro` and `.ts` files, validates field helper calls, detects invalid dynamic keys or cyclic groups, and reports diagnostics.
38
+ - `cms extract`: Extracts AST into canonical `SiteManifest` JSON containing all fields, groups, collections, and derived routes.
39
+ - `cms push`: Extracts manifest, verifies authentication, checks SHA-256 state hash in `.cms/state.json`, inserts collection seed entries once into empty collections, and synchronizes new/updated fields, collection schemas, and routes to the CMS Write API (`PUT /sites/:id/manifest`). Run `--dry-run` to preview changes and collections that would seed; run `--force` to bypass hash caching.
40
+ - `cms skills install [dir]`: Copies bundled agent skills into `<dir>/.claude/skills/`. Files edited locally are kept and reported unless `--force` is passed. `cms skills list` prints the bundled names.
41
+
42
+ ## Patterns
43
+
44
+ - **Auth Resolution Precedence**:
45
+ 1. Explicit environment token: `CMS_AUTH_TOKEN` or `SUPABASE_ACCESS_TOKEN` (supports deploy tokens `cms_dt_*`).
46
+ 2. Local credentials file (`~/.config/407dev/credentials.json`), auto-refreshed via Supabase if expiring within 60s.
47
+ 3. Ephemeral dev JWT auto-signed if running against loopback (`127.0.0.1` or `localhost`).
48
+ 4. Exit with error: `Not logged in to <api>. Run: cms login`.
49
+ - **Credentials Security**: Local directory created with `0700` permissions; `credentials.json` written with `0600` permissions.
50
+ - **Isolated CLI Sessions**: `POST /cli/sessions` issues a separate magic-link session for CLI usage to prevent Supabase refresh token rotation collisions with browser sessions.
51
+ - **Safe Env Merging**: `cms link` updates target CMS keys in `.env` without modifying unrelated variables, ordering, comments, or formatting.
52
+ - **Static AST Extraction**: Walks `.astro` (via `@astrojs/compiler` TSX conversion) and `.ts` files to derive `SiteManifest` automatically from `f.<type>()`, `<f.<type> />`, `f.scope()`, `f.group()`, and `defineCollection()`.
53
+ - **Bare Specifier Source Resolution**: Resolves bare package imports (e.g. `@407dev/cms-astro/collections`) via `package.json` `exports` mapping containing a `"source"` condition.
54
+ - **Sourcemap Position Tracing**: Traces original `file:line:col` in `.astro` frontmatter and JSX templates for exact error diagnostic reporting.
55
+ - **Group Multi-Hop Restriction**: Enforces 1-hop maximum for imported groups so static extraction remains deterministic without full project type-checking.
56
+ - **State Hashing & Skip**: Computes SHA-256 hash of canonical manifest JSON and caches in `.cms/state.json`. Skips API push if hash is unchanged (override with `--force`).
57
+
58
+ ## Integrations
59
+
60
+ - Consumes `@407dev/api-client` ([packages/api-client](../api-client)) for `pushManifest`, `listEntries`, `updateEntry`, `getSites`, `createCliSession`.
61
+ - Consumes `@407dev/blocks` ([packages/blocks](../blocks)) for `hashSchema`.
62
+ - Consumes `@407dev/field-types` ([packages/field-types](../field-types)) for field types and registry.
63
+ - Uses `@supabase/supabase-js` for refreshing local CLI sessions.
64
+
65
+ ## Constraints
66
+
67
+ - Published to public npm with executable permissions (`bin.cms`) and ESM build via `tsup`.
68
+ - Any export or command change requires a changeset (`pnpm changeset`).
69
+ - Zero dependencies on application packages (`apps/*`).
70
+
71
+ ## Gotchas
72
+
73
+ - In CI pipelines, set `CMS_AUTH_TOKEN=<site-deploy-token>` to authenticate `cms push` without browser logins.
74
+ - Deploy tokens (`cms_dt_*`) authorize manifest pushes, orphan previews, and migrations for their designated site only; central user operations are rejected.
75
+ - Field keys and scope prefixes passed to field helpers MUST be static string literals (dynamic variables or template strings produce extractor errors).
76
+ - Group and collection definitions must be in-file or imported across at most 1 hop.
77
+ - Bare package imports used in top-level definitions MUST declare a `"source"` export condition in `package.json` or extraction fails with an error diagnostic.
78
+ - `skills/**` ships in the npm tarball and is what agents in site repos follow; update it in the same commit as any change to cms-astro helpers, extractor rules, or CLI flags.
79
+ - `apply_manifest` sets `group` on each field definition (`field.group`), joining multiple source paths into a sorted comma-separated string.
@@ -0,0 +1,313 @@
1
+ import { createApiClient, SiteManifest, ManifestPushReport, SiteRecord, SiteManifestRoute } from '@407dev/api-client';
2
+ import { TraceMap } from '@jridgewell/trace-mapping';
3
+ import ts from 'typescript';
4
+
5
+ type ApiClient = ReturnType<typeof createApiClient>;
6
+ interface CmsCliState {
7
+ sites: Record<string, {
8
+ manifestHash: string;
9
+ lastPushedAt: string;
10
+ }>;
11
+ }
12
+ interface EntryMigration {
13
+ collection: string;
14
+ fromVersion: number;
15
+ toVersion: number;
16
+ migrate: (content: Record<string, unknown>) => Record<string, unknown>;
17
+ }
18
+ interface PushCommandOptions {
19
+ siteId: string;
20
+ siteDir?: string;
21
+ manifest?: SiteManifest;
22
+ configPath?: string;
23
+ renames?: Array<{
24
+ from: string;
25
+ to: string;
26
+ }>;
27
+ force?: boolean;
28
+ dryRun?: boolean;
29
+ check?: boolean;
30
+ yes?: boolean;
31
+ noSync?: boolean;
32
+ statePath?: string;
33
+ apiClient?: ApiClient;
34
+ migrations?: EntryMigration[];
35
+ /** Prompts before an orphaning push; defaults to a stdin y/N prompt. Injectable for tests/non-interactive callers. */
36
+ confirm?: (message: string) => Promise<boolean>;
37
+ }
38
+ interface PushCommandResult {
39
+ success: boolean;
40
+ skipped?: boolean;
41
+ aborted?: boolean;
42
+ hash: string;
43
+ report?: ManifestPushReport;
44
+ migrationsRun?: number;
45
+ warnings: string[];
46
+ errors: string[];
47
+ }
48
+
49
+ declare function resolveAuthToken(apiUrl?: string, customCredentialsPath?: string): Promise<string>;
50
+
51
+ interface StoredCredentials {
52
+ access_token: string;
53
+ refresh_token: string;
54
+ expires_at: number;
55
+ email?: string;
56
+ }
57
+ declare function normalizeApiUrl(url: string): string;
58
+ declare function getDefaultCredentialsPath(): string;
59
+ declare function loadAllCredentials(customPath?: string): Record<string, StoredCredentials>;
60
+ declare function getStoredCredentials(apiUrl: string, customPath?: string): StoredCredentials | null;
61
+ declare function saveStoredCredentials(apiUrl: string, creds: StoredCredentials, customPath?: string): void;
62
+ declare function removeStoredCredentials(apiUrl: string, customPath?: string): boolean;
63
+ declare function refreshCredentialsIfNeeded(apiUrl: string, creds: StoredCredentials, customPath?: string, fetchFn?: typeof fetch): Promise<StoredCredentials>;
64
+
65
+ interface CheckCommandOptions {
66
+ siteDir?: string;
67
+ noSync?: boolean;
68
+ }
69
+ interface CheckCommandResult {
70
+ success: boolean;
71
+ errorsCount: number;
72
+ warningsCount: number;
73
+ report: string;
74
+ }
75
+ declare function executeCheck(options?: CheckCommandOptions): Promise<CheckCommandResult>;
76
+
77
+ interface ExtractCommandOptions {
78
+ siteDir?: string;
79
+ noSync?: boolean;
80
+ json?: boolean;
81
+ }
82
+ declare function executeExtract(options?: ExtractCommandOptions): Promise<string>;
83
+
84
+ interface LinkCommandOptions {
85
+ siteId?: string;
86
+ apiUrl?: string;
87
+ siteDir?: string;
88
+ apiClient?: ApiClient;
89
+ selectPrompt?: (sites: SiteRecord[]) => Promise<SiteRecord>;
90
+ }
91
+ declare function mergeEnvContent(content: string, vars: Record<string, string>): string;
92
+ declare function mergeEnvFile(filePath: string, vars: Record<string, string>): void;
93
+ declare function executeLink(options?: LinkCommandOptions): Promise<string>;
94
+
95
+ interface LoginCommandOptions {
96
+ apiUrl?: string;
97
+ openBrowser?: boolean;
98
+ timeoutMs?: number;
99
+ }
100
+ declare function executeLogin(options?: LoginCommandOptions): Promise<string>;
101
+
102
+ interface LogoutCommandOptions {
103
+ apiUrl?: string;
104
+ customCredentialsPath?: string;
105
+ }
106
+ declare function executeLogout(options?: LogoutCommandOptions): string;
107
+
108
+ declare function executePush(options: PushCommandOptions): Promise<PushCommandResult>;
109
+
110
+ interface SkillsInstallOptions {
111
+ targetDir?: string;
112
+ force?: boolean;
113
+ sourceDir?: string;
114
+ }
115
+ interface SkillInstallResult {
116
+ name: string;
117
+ status: 'installed' | 'updated' | 'unchanged' | 'conflict';
118
+ conflicts: string[];
119
+ }
120
+ declare function resolveBundledSkillsDir(): string;
121
+ declare function listBundledSkills(sourceDir?: string): string[];
122
+ declare function installSkills(options?: SkillsInstallOptions): SkillInstallResult[];
123
+ declare function executeSkillsInstall(options?: SkillsInstallOptions): string;
124
+ declare function executeSkillsList(): string;
125
+
126
+ interface WhoamiCommandOptions {
127
+ apiUrl?: string;
128
+ customCredentialsPath?: string;
129
+ }
130
+ declare function executeWhoami(options?: WhoamiCommandOptions): Promise<string>;
131
+
132
+ interface SourceLocation {
133
+ file: string;
134
+ line: number;
135
+ column: number;
136
+ }
137
+ interface ExtractedField {
138
+ key: string;
139
+ type: string;
140
+ constraints?: Record<string, unknown>;
141
+ defaultValue?: unknown;
142
+ group?: string;
143
+ location: SourceLocation;
144
+ }
145
+ interface ExtractedCollection {
146
+ key: string;
147
+ jsonSchema: Record<string, unknown>;
148
+ schemaVersion: number;
149
+ orderable?: boolean;
150
+ routePattern?: string;
151
+ presentation?: 'visual' | 'form';
152
+ titleField?: string;
153
+ listColumns?: string[];
154
+ bodyField?: string;
155
+ group?: string;
156
+ seed?: Array<{
157
+ slug: string;
158
+ content: Record<string, unknown>;
159
+ }>;
160
+ location: SourceLocation;
161
+ }
162
+ interface ExtractedGroup {
163
+ groupName: string;
164
+ shape: Record<string, string>;
165
+ location: SourceLocation;
166
+ }
167
+ interface ExtractedScope {
168
+ varName: string;
169
+ prefix: string;
170
+ location: SourceLocation;
171
+ }
172
+ interface ExtractionDiagnostic {
173
+ type: 'error' | 'warning';
174
+ message: string;
175
+ file: string;
176
+ line: number;
177
+ column: number;
178
+ code?: string;
179
+ }
180
+ interface FileExtractionResult {
181
+ file: string;
182
+ fields: ExtractedField[];
183
+ collections: ExtractedCollection[];
184
+ groups: ExtractedGroup[];
185
+ scopes: ExtractedScope[];
186
+ diagnostics: ExtractionDiagnostic[];
187
+ }
188
+ interface MergedField {
189
+ type: string;
190
+ constraints?: Record<string, unknown>;
191
+ defaultValue?: unknown;
192
+ group?: string;
193
+ sources: SourceLocation[];
194
+ }
195
+ interface MergedExtractionResult {
196
+ fields: Record<string, MergedField>;
197
+ collections: Record<string, ExtractedCollection>;
198
+ routes: SiteManifestRoute[];
199
+ diagnostics: ExtractionDiagnostic[];
200
+ }
201
+
202
+ interface ExtractOptions {
203
+ siteDir?: string;
204
+ noSync?: boolean;
205
+ }
206
+ interface ExtractResult {
207
+ manifest: SiteManifest;
208
+ diagnostics: ExtractionDiagnostic[];
209
+ errors: ExtractionDiagnostic[];
210
+ warnings: ExtractionDiagnostic[];
211
+ merged: MergedExtractionResult;
212
+ }
213
+ declare function extractProject(siteDir?: string, options?: ExtractOptions): Promise<ExtractResult>;
214
+ declare function formatDiagnostics(diagnostics: ExtractionDiagnostic[]): string;
215
+
216
+ interface ParsedFile {
217
+ filePath: string;
218
+ relativePath: string;
219
+ isAstro: boolean;
220
+ sourceFile: ts.SourceFile;
221
+ traceMap?: TraceMap;
222
+ getLocation: (node: ts.Node) => SourceLocation;
223
+ }
224
+ declare function parseFile(filePath: string, rootPath?: string): Promise<ParsedFile>;
225
+ declare function globSourceFiles(dir: string): string[];
226
+
227
+ interface ResolvedGroup {
228
+ groupName: string;
229
+ shape: Record<string, string>;
230
+ definedLocation: SourceLocation;
231
+ hopCount: number;
232
+ }
233
+ interface ResolvedContext {
234
+ scopes: Map<string, {
235
+ prefix: string;
236
+ location: SourceLocation;
237
+ }>;
238
+ groups: Map<string, ResolvedGroup>;
239
+ groupInstances: Map<string, {
240
+ groupName: string;
241
+ prefix: string;
242
+ shape: Record<string, string>;
243
+ location: SourceLocation;
244
+ }>;
245
+ collections: Map<string, ExtractedCollection>;
246
+ diagnostics: ExtractionDiagnostic[];
247
+ }
248
+ declare function parseBareSpecifier(specifier: string): {
249
+ packageName: string;
250
+ subpath: string;
251
+ };
252
+ declare function findPackageRoot(fromFile: string, packageName: string): string | null;
253
+ interface ExportResolution {
254
+ resolvedPath: string | null;
255
+ hasSource: boolean;
256
+ exportFound: boolean;
257
+ }
258
+ declare function resolvePackageExport(pkgRoot: string, subpath: string): ExportResolution;
259
+ declare function resolveModulePath(fromFile: string, importSpecifier: string): string | null;
260
+ declare function getStringLiteralValue(node: ts.Node): string | null;
261
+ declare function extractGroupFromCall(callExpr: ts.CallExpression, fallbackName: string, sf: ts.SourceFile, diagnostics?: ExtractionDiagnostic[], getLocation?: (node: ts.Node) => SourceLocation): {
262
+ groupName: string;
263
+ shape: Record<string, string>;
264
+ } | null;
265
+ declare function resolveFileContext(parsed: ParsedFile, rootPath?: string, cachedParsedFiles?: Map<string, ParsedFile>): Promise<ResolvedContext>;
266
+
267
+ declare function collectFromFile(parsed: ParsedFile, context: ResolvedContext): FileExtractionResult;
268
+
269
+ declare function buildSiteManifest(merged: MergedExtractionResult, extraRoutes?: SiteManifestRoute[]): {
270
+ manifest: SiteManifest;
271
+ diagnostics: ExtractionDiagnostic[];
272
+ };
273
+
274
+ declare function mergeExtractionResults(fileResults: FileExtractionResult[]): MergedExtractionResult;
275
+
276
+ interface AstroResolvedRoute {
277
+ route?: string;
278
+ component?: string;
279
+ type?: string;
280
+ params?: string[];
281
+ }
282
+ declare function shouldSyncRoutes(siteDir: string, cmsRoutesFile: string): boolean;
283
+ declare function syncAstroRoutes(siteDir: string): boolean;
284
+ declare function deriveRouteFromComponent(componentPath: string): string;
285
+ declare function scanPagesDirectory(siteDir: string): AstroResolvedRoute[];
286
+ declare function loadDerivedRoutes(siteDir: string, collections?: Record<string, ExtractedCollection>, options?: {
287
+ noSync?: boolean;
288
+ }): {
289
+ routes: SiteManifestRoute[];
290
+ diagnostics: ExtractionDiagnostic[];
291
+ };
292
+
293
+ declare function loadManifest(pathOrObject?: string | SiteManifest): Promise<SiteManifest>;
294
+
295
+ declare function runEntryMigrations(apiClient: ApiClient, siteId: string, migrations: EntryMigration[]): Promise<{
296
+ migratedCount: number;
297
+ errors: string[];
298
+ }>;
299
+
300
+ declare class StateStore {
301
+ private filePath;
302
+ constructor(customPath?: string);
303
+ load(): CmsCliState;
304
+ getSiteManifestHash(siteId: string): string | null;
305
+ saveSiteManifestHash(siteId: string, hash: string): void;
306
+ }
307
+
308
+ declare const VERSION = "0.0.1";
309
+ declare function printHelp(): string;
310
+ declare function formatPushReport(result: PushCommandResult): string;
311
+ declare function runCli(args?: string[]): Promise<string>;
312
+
313
+ export { type ApiClient, type AstroResolvedRoute, type CheckCommandOptions, type CheckCommandResult, type CmsCliState, type EntryMigration, type ExportResolution, type ExtractCommandOptions, type ExtractOptions, type ExtractResult, type ExtractedCollection, type ExtractedField, type ExtractedGroup, type ExtractedScope, type ExtractionDiagnostic, type FileExtractionResult, type LinkCommandOptions, type LoginCommandOptions, type LogoutCommandOptions, type MergedExtractionResult, type MergedField, type ParsedFile, type PushCommandOptions, type PushCommandResult, type ResolvedContext, type ResolvedGroup, type SkillInstallResult, type SkillsInstallOptions, type SourceLocation, StateStore, type StoredCredentials, VERSION, type WhoamiCommandOptions, buildSiteManifest, collectFromFile, deriveRouteFromComponent, executeCheck, executeExtract, executeLink, executeLogin, executeLogout, executePush, executeSkillsInstall, executeSkillsList, executeWhoami, extractGroupFromCall, extractProject, findPackageRoot, formatDiagnostics, formatPushReport, getDefaultCredentialsPath, getStoredCredentials, getStringLiteralValue, globSourceFiles, installSkills, listBundledSkills, loadAllCredentials, loadDerivedRoutes, loadManifest, mergeEnvContent, mergeEnvFile, mergeExtractionResults, normalizeApiUrl, parseBareSpecifier, parseFile, printHelp, refreshCredentialsIfNeeded, removeStoredCredentials, resolveAuthToken, resolveBundledSkillsDir, resolveFileContext, resolveModulePath, resolvePackageExport, runCli, runEntryMigrations, saveStoredCredentials, scanPagesDirectory, shouldSyncRoutes, syncAstroRoutes };