flight-rules 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 CHANGED
@@ -89,6 +89,28 @@ Two core flows:
89
89
 
90
90
  Agents don't start these flows on their own; you explicitly invoke them.
91
91
 
92
+ ### Available Commands
93
+
94
+ Flight Rules provides workflow commands that agents can execute:
95
+
96
+ | Command | Purpose |
97
+ |---------|---------|
98
+ | `/dev-session.start` | Begin a structured coding session with goals and plan |
99
+ | `/dev-session.end` | End session, summarize work, update progress |
100
+ | `/prd.create` | Create a Product Requirements Document |
101
+ | `/prd.clarify` | Refine specific sections of an existing PRD |
102
+ | `/impl.outline` | Create implementation area structure |
103
+ | `/impl.create` | Add detailed tasks to implementation specs |
104
+ | `/feature.add` | Add a new feature to PRD and implementation docs |
105
+ | `/test.add` | Add tests for specific functionality |
106
+ | `/test.assess-current` | Analyze existing test coverage |
107
+ | `/prompt.refine` | Iteratively improve a prompt |
108
+ | `/readme.create` | Generate README from PRD and project state |
109
+ | `/readme.reconcile` | Update README to reflect recent changes |
110
+ | `/prd.reconcile` | Update PRD based on what was actually built |
111
+ | `/impl.reconcile` | Update implementation docs with status changes |
112
+ | `/docs.reconcile` | Run all reconcile commands + consistency check |
113
+
92
114
  ### Versioning
93
115
 
94
116
  Each project tracks which Flight Rules version it uses:
@@ -126,11 +148,22 @@ your-project/
126
148
  │ ├── session-log.md
127
149
  │ └── implementation/
128
150
  │ └── overview.md
129
- ├── commands/
151
+ ├── commands/ # Workflow commands
130
152
  │ ├── dev-session.start.md
131
153
  │ ├── dev-session.end.md
154
+ │ ├── prd.create.md
155
+ │ ├── prd.clarify.md
156
+ │ ├── impl.outline.md
157
+ │ ├── impl.create.md
158
+ │ ├── feature.add.md
132
159
  │ ├── test.add.md
133
- └── test.assess-current.md
160
+ ├── test.assess-current.md
161
+ │ ├── prompt.refine.md
162
+ │ ├── readme.create.md
163
+ │ ├── readme.reconcile.md
164
+ │ ├── prd.reconcile.md
165
+ │ ├── impl.reconcile.md
166
+ │ └── docs.reconcile.md
134
167
  └── prompts/ # Reusable prompt templates
135
168
  ```
136
169
 
@@ -0,0 +1 @@
1
+ export declare function update(args?: string[]): Promise<void>;
@@ -0,0 +1,137 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { spawn } from 'child_process';
4
+ import { checkForUpdate } from '../utils/version-check.js';
5
+ import { getChannel, setChannel } from '../utils/config.js';
6
+ import { isInteractive } from '../utils/interactive.js';
7
+ /**
8
+ * Parse --channel flag from args
9
+ */
10
+ function parseChannelArg(args) {
11
+ const channelIndex = args.findIndex(arg => arg === '--channel');
12
+ if (channelIndex !== -1 && args[channelIndex + 1]) {
13
+ const value = args[channelIndex + 1];
14
+ if (value === 'dev' || value === 'latest') {
15
+ return value;
16
+ }
17
+ }
18
+ // Also support --channel=dev format
19
+ const channelArg = args.find(arg => arg.startsWith('--channel='));
20
+ if (channelArg) {
21
+ const value = channelArg.split('=')[1];
22
+ if (value === 'dev' || value === 'latest') {
23
+ return value;
24
+ }
25
+ }
26
+ return undefined;
27
+ }
28
+ /**
29
+ * Run npm install to update the CLI
30
+ */
31
+ function runNpmInstall(channel) {
32
+ return new Promise((resolve) => {
33
+ const packageSpec = `flight-rules@${channel}`;
34
+ const npmProcess = spawn('npm', ['install', '-g', packageSpec], {
35
+ stdio: ['inherit', 'pipe', 'pipe'],
36
+ shell: true,
37
+ });
38
+ let stdout = '';
39
+ let stderr = '';
40
+ npmProcess.stdout?.on('data', (data) => {
41
+ stdout += data.toString();
42
+ });
43
+ npmProcess.stderr?.on('data', (data) => {
44
+ stderr += data.toString();
45
+ });
46
+ npmProcess.on('close', (code) => {
47
+ if (code === 0) {
48
+ resolve({ success: true });
49
+ }
50
+ else {
51
+ resolve({
52
+ success: false,
53
+ error: stderr || stdout || `npm exited with code ${code}`
54
+ });
55
+ }
56
+ });
57
+ npmProcess.on('error', (err) => {
58
+ resolve({ success: false, error: err.message });
59
+ });
60
+ });
61
+ }
62
+ export async function update(args = []) {
63
+ const interactive = isInteractive();
64
+ // Parse --channel flag
65
+ const requestedChannel = parseChannelArg(args);
66
+ const currentChannel = getChannel();
67
+ const targetChannel = requestedChannel || currentChannel;
68
+ // If switching channels, inform the user
69
+ if (requestedChannel && requestedChannel !== currentChannel) {
70
+ p.log.info(`Switching from ${pc.yellow(currentChannel)} to ${pc.cyan(requestedChannel)} channel.`);
71
+ }
72
+ // Check for updates (force fetch, bypass cache)
73
+ const spinner = p.spinner();
74
+ spinner.start('Checking for updates...');
75
+ const result = await checkForUpdate({ force: true });
76
+ if (!result) {
77
+ spinner.stop('Unable to check for updates');
78
+ p.log.error('Could not connect to npm registry. Check your network connection.');
79
+ return;
80
+ }
81
+ spinner.stop('Version check complete');
82
+ // Display version info
83
+ p.log.info(`Current version: ${pc.cyan(result.currentVersion)}`);
84
+ p.log.info(`Latest version (${targetChannel}): ${pc.cyan(result.latestVersion)}`);
85
+ // Check if update is needed
86
+ if (!result.updateAvailable && targetChannel === currentChannel) {
87
+ p.log.success('You are already on the latest version!');
88
+ p.outro(pc.green('No update needed.'));
89
+ return;
90
+ }
91
+ // If only switching channels (no version change), still proceed
92
+ const switchingChannels = requestedChannel && requestedChannel !== currentChannel;
93
+ if (!result.updateAvailable && !switchingChannels) {
94
+ p.log.success('You are already on the latest version!');
95
+ p.outro(pc.green('No update needed.'));
96
+ return;
97
+ }
98
+ // Non-interactive mode: show info but don't update
99
+ if (!interactive) {
100
+ if (result.updateAvailable) {
101
+ p.log.info(`Update available: ${pc.yellow(result.currentVersion)} → ${pc.cyan(result.latestVersion)}`);
102
+ p.log.message('Run this command in an interactive terminal to update.');
103
+ }
104
+ if (switchingChannels) {
105
+ p.log.info(`Run this command in an interactive terminal to switch to the ${requestedChannel} channel.`);
106
+ }
107
+ return;
108
+ }
109
+ // Interactive mode: confirm and update
110
+ const message = result.updateAvailable
111
+ ? `Update from ${result.currentVersion} to ${result.latestVersion}?`
112
+ : `Switch to ${targetChannel} channel?`;
113
+ const shouldUpdate = await p.confirm({
114
+ message,
115
+ initialValue: true,
116
+ });
117
+ if (p.isCancel(shouldUpdate) || !shouldUpdate) {
118
+ p.log.info('Update cancelled.');
119
+ return;
120
+ }
121
+ // Perform update
122
+ spinner.start(`Installing flight-rules@${targetChannel}...`);
123
+ const installResult = await runNpmInstall(targetChannel);
124
+ if (!installResult.success) {
125
+ spinner.stop('Update failed');
126
+ p.log.error(installResult.error || 'Unknown error during npm install');
127
+ p.log.message(`You can try manually: ${pc.cyan(`npm install -g flight-rules@${targetChannel}`)}`);
128
+ return;
129
+ }
130
+ spinner.stop('Update complete!');
131
+ // Update channel in config if it changed
132
+ if (requestedChannel && requestedChannel !== currentChannel) {
133
+ setChannel(requestedChannel);
134
+ p.log.success(`Channel set to ${pc.cyan(requestedChannel)}`);
135
+ }
136
+ p.outro(pc.green(`Successfully updated to flight-rules@${targetChannel}!`));
137
+ }
package/dist/index.js CHANGED
@@ -4,7 +4,10 @@ import pc from 'picocolors';
4
4
  import { init } from './commands/init.js';
5
5
  import { upgrade } from './commands/upgrade.js';
6
6
  import { adapter } from './commands/adapter.js';
7
+ import { update } from './commands/update.js';
7
8
  import { getCliVersion } from './utils/files.js';
9
+ import { checkForUpdate, shouldSkipUpdateCheck } from './utils/version-check.js';
10
+ import { isInteractive } from './utils/interactive.js';
8
11
  const command = process.argv[2];
9
12
  const args = process.argv.slice(3);
10
13
  /**
@@ -41,6 +44,9 @@ async function main() {
41
44
  case 'adapter':
42
45
  await adapter(args);
43
46
  break;
47
+ case 'update':
48
+ await update(args);
49
+ break;
44
50
  case undefined:
45
51
  case '--help':
46
52
  case '-h':
@@ -51,6 +57,8 @@ async function main() {
51
57
  showHelp();
52
58
  process.exit(1);
53
59
  }
60
+ // Show update notification after command completes (uses cache, non-blocking)
61
+ await showUpdateNotification();
54
62
  }
55
63
  function showHelp() {
56
64
  console.log(`
@@ -60,11 +68,16 @@ ${pc.bold('Commands:')}
60
68
  init Install Flight Rules into the current project
61
69
  upgrade Upgrade Flight Rules (preserves your docs)
62
70
  adapter Generate agent-specific adapter files
71
+ update Update the Flight Rules CLI itself
63
72
 
64
73
  ${pc.bold('Upgrade Options:')}
65
74
  --version <version> Upgrade to a specific version (e.g., 0.1.4)
66
75
  Defaults to latest from main branch
67
76
 
77
+ ${pc.bold('Update Options:')}
78
+ --channel <channel> Switch release channel (dev or latest)
79
+ Defaults to current channel
80
+
68
81
  ${pc.bold('Adapter Options:')}
69
82
  --cursor Generate AGENTS.md for Cursor
70
83
  --claude Generate CLAUDE.md for Claude Code
@@ -74,9 +87,37 @@ ${pc.bold('Examples:')}
74
87
  flight-rules init
75
88
  flight-rules upgrade
76
89
  flight-rules upgrade --version 0.1.4
90
+ flight-rules update
91
+ flight-rules update --channel=latest
77
92
  flight-rules adapter --cursor --claude
78
93
  `);
79
94
  }
95
+ /**
96
+ * Show update notification if a newer version is available.
97
+ * Uses cached check result to avoid slowing down commands.
98
+ * Only shows in interactive (TTY) mode.
99
+ */
100
+ async function showUpdateNotification() {
101
+ // Skip in non-interactive mode or if disabled
102
+ if (!isInteractive() || shouldSkipUpdateCheck()) {
103
+ return;
104
+ }
105
+ // Skip for commands that handle their own version checking
106
+ if (command === 'update' || command === '--version' || command === '-v') {
107
+ return;
108
+ }
109
+ try {
110
+ const result = await checkForUpdate(); // Uses cache, won't slow down
111
+ if (result?.updateAvailable) {
112
+ console.log();
113
+ p.log.message(pc.yellow(`Update available: ${result.currentVersion} → ${result.latestVersion}`) +
114
+ pc.dim(` Run 'flight-rules update' to upgrade.`));
115
+ }
116
+ }
117
+ catch {
118
+ // Silent failure - don't disrupt the user's workflow
119
+ }
120
+ }
80
121
  main().catch((err) => {
81
122
  p.log.error(err.message);
82
123
  process.exit(1);
@@ -0,0 +1,44 @@
1
+ /**
2
+ * User-level configuration for Flight Rules CLI
3
+ */
4
+ export interface UserConfig {
5
+ channel: 'dev' | 'latest';
6
+ lastUpdateCheck?: {
7
+ timestamp: string;
8
+ latestVersion: string;
9
+ };
10
+ }
11
+ /**
12
+ * Get the path to the user-level Flight Rules directory
13
+ */
14
+ export declare function getUserFlightRulesDir(): string;
15
+ /**
16
+ * Get the path to the user-level config file
17
+ */
18
+ export declare function getConfigPath(): string;
19
+ /**
20
+ * Read the user-level config, creating default if missing
21
+ */
22
+ export declare function readConfig(): UserConfig;
23
+ /**
24
+ * Write the user-level config
25
+ */
26
+ export declare function writeConfig(config: UserConfig): void;
27
+ /**
28
+ * Get the configured release channel
29
+ */
30
+ export declare function getChannel(): 'dev' | 'latest';
31
+ /**
32
+ * Set the release channel
33
+ */
34
+ export declare function setChannel(channel: 'dev' | 'latest'): void;
35
+ /**
36
+ * Update the cached update check result
37
+ */
38
+ export declare function updateLastCheck(latestVersion: string): void;
39
+ /**
40
+ * Get the cached update check result, if still valid
41
+ * @param maxAgeMs - Maximum age in milliseconds (default 24 hours)
42
+ * @returns The cached version if valid, null otherwise
43
+ */
44
+ export declare function getCachedVersion(maxAgeMs?: number): string | null;
@@ -0,0 +1,89 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ const DEFAULT_CONFIG = {
5
+ channel: 'dev',
6
+ };
7
+ /**
8
+ * Get the path to the user-level Flight Rules directory
9
+ */
10
+ export function getUserFlightRulesDir() {
11
+ return join(homedir(), '.flight-rules');
12
+ }
13
+ /**
14
+ * Get the path to the user-level config file
15
+ */
16
+ export function getConfigPath() {
17
+ return join(getUserFlightRulesDir(), 'config.json');
18
+ }
19
+ /**
20
+ * Read the user-level config, creating default if missing
21
+ */
22
+ export function readConfig() {
23
+ const configPath = getConfigPath();
24
+ if (!existsSync(configPath)) {
25
+ return { ...DEFAULT_CONFIG };
26
+ }
27
+ try {
28
+ const content = readFileSync(configPath, 'utf-8');
29
+ const parsed = JSON.parse(content);
30
+ // Merge with defaults to handle missing fields
31
+ return { ...DEFAULT_CONFIG, ...parsed };
32
+ }
33
+ catch {
34
+ return { ...DEFAULT_CONFIG };
35
+ }
36
+ }
37
+ /**
38
+ * Write the user-level config
39
+ */
40
+ export function writeConfig(config) {
41
+ const configPath = getConfigPath();
42
+ const configDir = dirname(configPath);
43
+ if (!existsSync(configDir)) {
44
+ mkdirSync(configDir, { recursive: true });
45
+ }
46
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
47
+ }
48
+ /**
49
+ * Get the configured release channel
50
+ */
51
+ export function getChannel() {
52
+ return readConfig().channel;
53
+ }
54
+ /**
55
+ * Set the release channel
56
+ */
57
+ export function setChannel(channel) {
58
+ const config = readConfig();
59
+ config.channel = channel;
60
+ writeConfig(config);
61
+ }
62
+ /**
63
+ * Update the cached update check result
64
+ */
65
+ export function updateLastCheck(latestVersion) {
66
+ const config = readConfig();
67
+ config.lastUpdateCheck = {
68
+ timestamp: new Date().toISOString(),
69
+ latestVersion,
70
+ };
71
+ writeConfig(config);
72
+ }
73
+ /**
74
+ * Get the cached update check result, if still valid
75
+ * @param maxAgeMs - Maximum age in milliseconds (default 24 hours)
76
+ * @returns The cached version if valid, null otherwise
77
+ */
78
+ export function getCachedVersion(maxAgeMs = 24 * 60 * 60 * 1000) {
79
+ const config = readConfig();
80
+ if (!config.lastUpdateCheck) {
81
+ return null;
82
+ }
83
+ const checkTime = new Date(config.lastUpdateCheck.timestamp).getTime();
84
+ const now = Date.now();
85
+ if (now - checkTime > maxAgeMs) {
86
+ return null; // Cache expired
87
+ }
88
+ return config.lastUpdateCheck.latestVersion;
89
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Result of a version check
3
+ */
4
+ export interface VersionCheckResult {
5
+ currentVersion: string;
6
+ latestVersion: string;
7
+ updateAvailable: boolean;
8
+ channel: 'dev' | 'latest';
9
+ }
10
+ /**
11
+ * Options for version check
12
+ */
13
+ export interface VersionCheckOptions {
14
+ force?: boolean;
15
+ }
16
+ /**
17
+ * Check for available updates
18
+ * @param options - Options for the check
19
+ * @returns Version check result, or null if check failed
20
+ */
21
+ export declare function checkForUpdate(options?: VersionCheckOptions): Promise<VersionCheckResult | null>;
22
+ /**
23
+ * Check if update check should be skipped based on environment
24
+ */
25
+ export declare function shouldSkipUpdateCheck(): boolean;
@@ -0,0 +1,87 @@
1
+ import { getCliVersion } from './files.js';
2
+ import { getChannel, getCachedVersion, updateLastCheck } from './config.js';
3
+ const NPM_REGISTRY_URL = 'https://registry.npmjs.org/flight-rules';
4
+ /**
5
+ * Compare two semver versions
6
+ * @returns true if v2 is newer than v1
7
+ */
8
+ function isNewerVersion(current, latest) {
9
+ const v1Parts = current.split('.').map(Number);
10
+ const v2Parts = latest.split('.').map(Number);
11
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
12
+ const v1 = v1Parts[i] || 0;
13
+ const v2 = v2Parts[i] || 0;
14
+ if (v2 > v1)
15
+ return true;
16
+ if (v2 < v1)
17
+ return false;
18
+ }
19
+ return false;
20
+ }
21
+ /**
22
+ * Fetch the latest version from npm registry
23
+ * @returns The latest version for the given channel, or null on error
24
+ */
25
+ async function fetchLatestVersion(channel) {
26
+ try {
27
+ const response = await fetch(NPM_REGISTRY_URL, {
28
+ headers: { 'Accept': 'application/json' },
29
+ });
30
+ if (!response.ok) {
31
+ return null;
32
+ }
33
+ const data = await response.json();
34
+ const distTags = data['dist-tags'];
35
+ if (!distTags) {
36
+ return null;
37
+ }
38
+ return distTags[channel] || null;
39
+ }
40
+ catch {
41
+ // Silent failure on network errors
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Check for available updates
47
+ * @param options - Options for the check
48
+ * @returns Version check result, or null if check failed
49
+ */
50
+ export async function checkForUpdate(options = {}) {
51
+ const currentVersion = getCliVersion();
52
+ const channel = getChannel();
53
+ if (currentVersion === 'unknown') {
54
+ return null;
55
+ }
56
+ // Check cache first (unless forced)
57
+ if (!options.force) {
58
+ const cachedVersion = getCachedVersion();
59
+ if (cachedVersion) {
60
+ return {
61
+ currentVersion,
62
+ latestVersion: cachedVersion,
63
+ updateAvailable: isNewerVersion(currentVersion, cachedVersion),
64
+ channel,
65
+ };
66
+ }
67
+ }
68
+ // Fetch from npm registry
69
+ const latestVersion = await fetchLatestVersion(channel);
70
+ if (!latestVersion) {
71
+ return null; // Network error or registry issue
72
+ }
73
+ // Update cache
74
+ updateLastCheck(latestVersion);
75
+ return {
76
+ currentVersion,
77
+ latestVersion,
78
+ updateAvailable: isNewerVersion(currentVersion, latestVersion),
79
+ channel,
80
+ };
81
+ }
82
+ /**
83
+ * Check if update check should be skipped based on environment
84
+ */
85
+ export function shouldSkipUpdateCheck() {
86
+ return process.env.FLIGHT_RULES_NO_UPDATE_CHECK === '1';
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flight-rules",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "An opinionated framework for AI-assisted software development",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/payload/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Flight Rules – Agent Guidelines
2
2
 
3
- flight_rules_version: 0.8.0
3
+ flight_rules_version: 0.10.0
4
4
 
5
5
  This file defines how agents (Claude Code, Cursor, etc.) should work on software projects using the Flight Rules system.
6
6