create-claude-workspace 1.1.34 → 1.1.36
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/template/.claude/profiles/angular.md +5 -1
- package/dist/template/.claude/profiles/react.md +5 -1
- package/dist/template/.claude/profiles/svelte.md +5 -1
- package/dist/template/.claude/profiles/vue.md +5 -1
- package/dist/template/.claude/scripts/autonomous.mjs +49 -9
- package/dist/template/.claude/scripts/lib/claude-runner.mjs +2 -2
- package/dist/template/.claude/scripts/lib/errors.mjs +7 -1
- package/dist/template/.claude/scripts/lib/oauth-refresh.mjs +146 -0
- package/package.json +1 -1
|
@@ -175,7 +175,11 @@ npx themecraft generate # generates type-safe SCSS from tokens.json
|
|
|
175
175
|
|
|
176
176
|
**How it works:** `npx themecraft generate` reads `tokens.json` and produces typed SCSS files (`generated/theme/colors.scss`, `sizes.scss`, `typography.scss`) that wrap `color-var()`/`size-var()` internally. Components consume these generated variables — never the raw accessor functions.
|
|
177
177
|
|
|
178
|
-
**
|
|
178
|
+
**Token source (pick one):**
|
|
179
|
+
- **With Figma:** `npx themecraft figma-sync` pulls design variables into `tokens.json`, then `npx themecraft generate`
|
|
180
|
+
- **Without Figma:** `npx themecraft init` creates `tokens.json` scaffold → manually define token names (color groups, size groups, typography levels) → `npx themecraft generate`
|
|
181
|
+
|
|
182
|
+
Either way, the result is the same: typed SCSS files in `generated/theme/` ready to import.
|
|
179
183
|
|
|
180
184
|
**Theme definition** (`themes/light.scss`):
|
|
181
185
|
```scss
|
|
@@ -185,7 +185,11 @@ npx themecraft generate # generates type-safe SCSS from tokens.json
|
|
|
185
185
|
|
|
186
186
|
**How it works:** `npx themecraft generate` reads `tokens.json` and produces typed SCSS files (`generated/theme/colors.scss`, `sizes.scss`, `typography.scss`) that wrap `color-var()`/`size-var()` internally. Components consume these generated variables — never the raw accessor functions.
|
|
187
187
|
|
|
188
|
-
**
|
|
188
|
+
**Token source (pick one):**
|
|
189
|
+
- **With Figma:** `npx themecraft figma-sync` pulls design variables into `tokens.json`, then `npx themecraft generate`
|
|
190
|
+
- **Without Figma:** `npx themecraft init` creates `tokens.json` scaffold → manually define token names (color groups, size groups, typography levels) → `npx themecraft generate`
|
|
191
|
+
|
|
192
|
+
Either way, the result is the same: typed SCSS files in `generated/theme/` ready to import.
|
|
189
193
|
|
|
190
194
|
**Runtime setup** (in layout or `_app.tsx`):
|
|
191
195
|
```typescript
|
|
@@ -188,7 +188,11 @@ npx themecraft generate # generates type-safe SCSS from tokens.json
|
|
|
188
188
|
|
|
189
189
|
**How it works:** `npx themecraft generate` reads `tokens.json` and produces typed SCSS files (`generated/theme/colors.scss`, `sizes.scss`, `typography.scss`) that wrap `color-var()`/`size-var()` internally. Components consume these generated variables — never the raw accessor functions.
|
|
190
190
|
|
|
191
|
-
**
|
|
191
|
+
**Token source (pick one):**
|
|
192
|
+
- **With Figma:** `npx themecraft figma-sync` pulls design variables into `tokens.json`, then `npx themecraft generate`
|
|
193
|
+
- **Without Figma:** `npx themecraft init` creates `tokens.json` scaffold → manually define token names (color groups, size groups, typography levels) → `npx themecraft generate`
|
|
194
|
+
|
|
195
|
+
Either way, the result is the same: typed SCSS files in `generated/theme/` ready to import.
|
|
192
196
|
|
|
193
197
|
**Runtime setup** (in `+layout.svelte` or `hooks.server.ts`):
|
|
194
198
|
```typescript
|
|
@@ -213,7 +213,11 @@ npx themecraft generate # generates type-safe SCSS from tokens.json
|
|
|
213
213
|
|
|
214
214
|
**How it works:** `npx themecraft generate` reads `tokens.json` and produces typed SCSS files (`generated/theme/colors.scss`, `sizes.scss`, `typography.scss`) that wrap `color-var()`/`size-var()` internally. Components consume these generated variables — never the raw accessor functions.
|
|
215
215
|
|
|
216
|
-
**
|
|
216
|
+
**Token source (pick one):**
|
|
217
|
+
- **With Figma:** `npx themecraft figma-sync` pulls design variables into `tokens.json`, then `npx themecraft generate`
|
|
218
|
+
- **Without Figma:** `npx themecraft init` creates `tokens.json` scaffold → manually define token names (color groups, size groups, typography levels) → `npx themecraft generate`
|
|
219
|
+
|
|
220
|
+
Either way, the result is the same: typed SCSS files in `generated/theme/` ready to import.
|
|
217
221
|
|
|
218
222
|
**Runtime setup** (in `App.vue` or plugin):
|
|
219
223
|
```typescript
|
|
@@ -9,6 +9,7 @@ import { createLogger } from './lib/logger.mjs';
|
|
|
9
9
|
import { emptyCheckpoint, readCheckpoint, writeCheckpoint } from './lib/state.mjs';
|
|
10
10
|
import { runClaude, currentChild } from './lib/claude-runner.mjs';
|
|
11
11
|
import { sleep, formatDuration, acquireLock, releaseLock, readMemory, isProjectComplete, getCurrentTask, getCurrentPhase, checkClaudeInstalled, checkAuth, checkGitIdentity, checkFilesystemWritable, gitFetchAndPull, gitCheckState, notify, printSummary, promptUser, } from './lib/utils.mjs';
|
|
12
|
+
import { isTokenExpiringSoon, refreshOAuthToken } from './lib/oauth-refresh.mjs';
|
|
12
13
|
// ─── Args ───
|
|
13
14
|
function parseArgs(argv) {
|
|
14
15
|
const opts = { ...DEFAULTS };
|
|
@@ -291,18 +292,34 @@ async function main() {
|
|
|
291
292
|
if (timeSinceLastAuth > opts.processTimeout * 2) {
|
|
292
293
|
log.warn('Large time gap detected (system sleep?). Re-checking auth...');
|
|
293
294
|
if (!checkAuth()) {
|
|
294
|
-
log.
|
|
295
|
-
|
|
296
|
-
|
|
295
|
+
log.warn('Auth expired after sleep. Attempting OAuth refresh...');
|
|
296
|
+
const refreshed = await refreshOAuthToken(log);
|
|
297
|
+
if (!refreshed || !checkAuth()) {
|
|
298
|
+
log.error('Auth expired after sleep and refresh failed. Run `claude /login`.');
|
|
299
|
+
notify(opts.notifyCommand, 'stopped', 'Auth expired after system sleep (refresh failed)', i);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
log.info('OAuth token refreshed after sleep. Continuing.');
|
|
297
303
|
}
|
|
298
304
|
checkpoint.lastAuthCheckTime = Date.now();
|
|
299
305
|
}
|
|
300
|
-
//
|
|
301
|
-
if (
|
|
306
|
+
// Proactive token refresh — refresh before expiry to prevent mid-iteration failures
|
|
307
|
+
if (isTokenExpiringSoon()) {
|
|
308
|
+
log.info('OAuth token expiring soon. Refreshing proactively...');
|
|
309
|
+
await refreshOAuthToken(log);
|
|
310
|
+
}
|
|
311
|
+
// Periodic auth re-check (every 10 min)
|
|
312
|
+
if (timeSinceLastAuth > 10 * 60_000) {
|
|
302
313
|
if (!checkAuth()) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
314
|
+
// Try OAuth refresh before giving up
|
|
315
|
+
log.warn('Auth check failed. Attempting OAuth token refresh...');
|
|
316
|
+
const refreshed = await refreshOAuthToken(log);
|
|
317
|
+
if (!refreshed || !checkAuth()) {
|
|
318
|
+
log.error('Auth expired and refresh failed. Run `claude /login` or check ANTHROPIC_API_KEY.');
|
|
319
|
+
notify(opts.notifyCommand, 'stopped', 'Auth expired (refresh failed)', i);
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
log.info('OAuth token refreshed successfully. Continuing.');
|
|
306
323
|
}
|
|
307
324
|
checkpoint.lastAuthCheckTime = Date.now();
|
|
308
325
|
}
|
|
@@ -333,14 +350,37 @@ async function main() {
|
|
|
333
350
|
stats.processTimeouts++;
|
|
334
351
|
if (category === 'activity_timeout')
|
|
335
352
|
stats.activityTimeouts++;
|
|
353
|
+
// Immediate auth check after any error — catches expired OAuth tokens that
|
|
354
|
+
// weren't classified as auth_expired (e.g., process_timeout with auth errors in stderr)
|
|
355
|
+
if (category !== 'auth_expired' && !checkAuth()) {
|
|
356
|
+
log.warn('Auth check failed after error. Attempting OAuth refresh...');
|
|
357
|
+
const refreshed = await refreshOAuthToken(log);
|
|
358
|
+
if (!refreshed || !checkAuth()) {
|
|
359
|
+
log.error('Auth expired and refresh failed. Run `claude /login` or check ANTHROPIC_API_KEY.');
|
|
360
|
+
notify(opts.notifyCommand, 'stopped', 'Auth expired (refresh failed after process error)', i);
|
|
361
|
+
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
log.info('OAuth token refreshed after error. Continuing.');
|
|
365
|
+
}
|
|
336
366
|
}
|
|
337
367
|
// Get response action
|
|
338
368
|
const action = getErrorAction(category, {
|
|
339
369
|
consecutiveFailures: checkpoint.consecutiveFailures,
|
|
340
370
|
authRetries: checkpoint.authRetries,
|
|
341
371
|
});
|
|
342
|
-
// Handle stop
|
|
372
|
+
// Handle stop — try OAuth refresh for auth errors before giving up
|
|
343
373
|
if (action.type === 'stop') {
|
|
374
|
+
if (category === 'auth_expired') {
|
|
375
|
+
log.warn('Auth expired. Attempting OAuth token refresh...');
|
|
376
|
+
const refreshed = await refreshOAuthToken(log);
|
|
377
|
+
if (refreshed && checkAuth()) {
|
|
378
|
+
log.info('OAuth token refreshed. Retrying iteration.');
|
|
379
|
+
checkpoint.lastAuthCheckTime = Date.now();
|
|
380
|
+
i--; // Retry this iteration
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
344
384
|
log.error(action.reason);
|
|
345
385
|
notify(opts.notifyCommand, 'stopped', action.reason, i);
|
|
346
386
|
writeCheckpoint(opts.projectDir, checkpoint, log);
|
|
@@ -352,9 +352,9 @@ export function runClaude(opts, log, runOpts = {}) {
|
|
|
352
352
|
// Kill on auth error burst (e.g. expired token causing infinite retry loop)
|
|
353
353
|
if (AUTH_ERROR_RE.test(lower)) {
|
|
354
354
|
authErrorCount++;
|
|
355
|
-
if (authErrorCount >=
|
|
355
|
+
if (authErrorCount >= 3 && !killed) {
|
|
356
356
|
log.warn(`Auth error burst detected (${authErrorCount} errors) — killing process.`);
|
|
357
|
-
killChild('
|
|
357
|
+
killChild('process_timeout');
|
|
358
358
|
return;
|
|
359
359
|
}
|
|
360
360
|
}
|
|
@@ -15,14 +15,20 @@ export function classifyError(signals) {
|
|
|
15
15
|
if (signals.code === 0 && signals.hasResult)
|
|
16
16
|
return 'none';
|
|
17
17
|
// Flag-based (set by stream events, not just stderr)
|
|
18
|
+
// Auth errors take priority over everything — prevents 30-min timeout loops on expired tokens
|
|
18
19
|
if (signals.isAuthError)
|
|
19
20
|
return 'auth_expired';
|
|
20
21
|
if (signals.isAuthServerError)
|
|
21
22
|
return 'auth_server_error';
|
|
22
23
|
if (signals.isRateLimit)
|
|
23
24
|
return 'rate_limited';
|
|
24
|
-
if (signals.timedOut)
|
|
25
|
+
if (signals.timedOut) {
|
|
26
|
+
// Double-check stderr for auth errors (may not have been caught by stream events)
|
|
27
|
+
if (/authentication_error|failed to authenticate|api error: 401|oauth token has expired/i.test(signals.stderr)) {
|
|
28
|
+
return 'auth_expired';
|
|
29
|
+
}
|
|
25
30
|
return 'process_timeout';
|
|
31
|
+
}
|
|
26
32
|
// Activity timeout after result = success (process just hung after finishing)
|
|
27
33
|
if (signals.activityTimedOut)
|
|
28
34
|
return signals.hasResult ? 'none' : 'activity_timeout';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// ─── OAuth token refresh for Claude Code ───
|
|
2
|
+
// Cross-platform: reads/writes ~/.claude/.credentials.json (Windows/Linux)
|
|
3
|
+
// or macOS Keychain via `security` CLI.
|
|
4
|
+
// Refresh endpoint: https://console.anthropic.com/v1/oauth/token
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
const TOKEN_URL = 'https://console.anthropic.com/v1/oauth/token';
|
|
10
|
+
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
11
|
+
const CREDENTIALS_FILE = '.claude/.credentials.json';
|
|
12
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
13
|
+
// Buffer: refresh if token expires within this many ms
|
|
14
|
+
const REFRESH_BUFFER_MS = 30 * 60_000; // 30 minutes
|
|
15
|
+
// ─── Credential storage (cross-platform) ───
|
|
16
|
+
function credentialsPath() {
|
|
17
|
+
return resolve(homedir(), CREDENTIALS_FILE);
|
|
18
|
+
}
|
|
19
|
+
function readCredentialsFile() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(credentialsPath(), 'utf-8');
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed?.claudeAiOauth?.refreshToken)
|
|
24
|
+
return parsed;
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function writeCredentialsFile(creds) {
|
|
32
|
+
writeFileSync(credentialsPath(), JSON.stringify(creds));
|
|
33
|
+
}
|
|
34
|
+
function readKeychainCredentials() {
|
|
35
|
+
try {
|
|
36
|
+
// Try common account names
|
|
37
|
+
const accounts = ['claude', 'Claude Code', 'default'];
|
|
38
|
+
for (const account of accounts) {
|
|
39
|
+
try {
|
|
40
|
+
const raw = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}" -w`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 5000 }).trim();
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (parsed?.claudeAiOauth?.refreshToken)
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch { /* try next account */ }
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function writeKeychainCredentials(creds, account) {
|
|
54
|
+
const data = JSON.stringify(creds);
|
|
55
|
+
try {
|
|
56
|
+
execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}"`, { stdio: 'ignore', timeout: 5000 });
|
|
57
|
+
}
|
|
58
|
+
catch { /* may not exist */ }
|
|
59
|
+
execSync(`security add-generic-password -s "${KEYCHAIN_SERVICE}" -a "${account}" -w '${data.replace(/'/g, "'\\''")}' -U`, { stdio: 'ignore', timeout: 5000 });
|
|
60
|
+
}
|
|
61
|
+
function readCredentials() {
|
|
62
|
+
// File-based (Windows/Linux) takes priority — always present
|
|
63
|
+
const fileCreds = readCredentialsFile();
|
|
64
|
+
if (fileCreds)
|
|
65
|
+
return fileCreds;
|
|
66
|
+
// macOS Keychain fallback
|
|
67
|
+
if (process.platform === 'darwin')
|
|
68
|
+
return readKeychainCredentials();
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function writeCredentials(creds) {
|
|
72
|
+
// Always write to file
|
|
73
|
+
writeCredentialsFile(creds);
|
|
74
|
+
// Also update Keychain on macOS
|
|
75
|
+
if (process.platform === 'darwin') {
|
|
76
|
+
try {
|
|
77
|
+
writeKeychainCredentials(creds, 'claude');
|
|
78
|
+
}
|
|
79
|
+
catch { /* non-fatal */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ─── Token refresh ───
|
|
83
|
+
async function postRefresh(refreshToken) {
|
|
84
|
+
const body = JSON.stringify({
|
|
85
|
+
grant_type: 'refresh_token',
|
|
86
|
+
refresh_token: refreshToken,
|
|
87
|
+
client_id: CLIENT_ID,
|
|
88
|
+
});
|
|
89
|
+
const response = await fetch(TOKEN_URL, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body,
|
|
93
|
+
signal: AbortSignal.timeout(30_000),
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const text = await response.text().catch(() => '');
|
|
97
|
+
throw new Error(`OAuth refresh failed: ${response.status} ${text.slice(0, 200)}`);
|
|
98
|
+
}
|
|
99
|
+
return await response.json();
|
|
100
|
+
}
|
|
101
|
+
// ─── Public API ───
|
|
102
|
+
/** Check if the current OAuth token is expired or about to expire. */
|
|
103
|
+
export function isTokenExpiringSoon() {
|
|
104
|
+
const creds = readCredentials();
|
|
105
|
+
if (!creds)
|
|
106
|
+
return false; // No OAuth credentials (API key user) — not our problem
|
|
107
|
+
return Date.now() > (creds.claudeAiOauth.expiresAt - REFRESH_BUFFER_MS);
|
|
108
|
+
}
|
|
109
|
+
/** Attempt to refresh the OAuth token. Returns true on success. */
|
|
110
|
+
export async function refreshOAuthToken(log) {
|
|
111
|
+
const creds = readCredentials();
|
|
112
|
+
if (!creds) {
|
|
113
|
+
log.warn('No OAuth credentials found — cannot refresh.');
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const oauth = creds.claudeAiOauth;
|
|
117
|
+
if (!oauth.refreshToken) {
|
|
118
|
+
log.warn('No refresh token available.');
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
const remaining = oauth.expiresAt - Date.now();
|
|
122
|
+
log.info(`Token expires in ${Math.round(remaining / 60_000)}m. Attempting refresh...`);
|
|
123
|
+
try {
|
|
124
|
+
const result = await postRefresh(oauth.refreshToken);
|
|
125
|
+
if (!result.access_token || !result.refresh_token) {
|
|
126
|
+
log.error('Refresh response missing tokens.');
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
// Update credentials
|
|
130
|
+
const updated = {
|
|
131
|
+
claudeAiOauth: {
|
|
132
|
+
...oauth,
|
|
133
|
+
accessToken: result.access_token,
|
|
134
|
+
refreshToken: result.refresh_token,
|
|
135
|
+
expiresAt: Date.now() + (result.expires_in * 1000),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
writeCredentials(updated);
|
|
139
|
+
log.info(`OAuth token refreshed. New expiry: ${new Date(updated.claudeAiOauth.expiresAt).toISOString()}`);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
log.error(`OAuth refresh failed: ${err.message}`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|